Съдържание на курса
Урок 1 — Основи
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 45 мин.
Какво изграждаш: функциите и базовите типове на revisor
Какво научаваш: променливи и mut, засенчване, скаларни и съставни типове, функции, „всичко е израз“, if, loop, while и for
След края ще можеш да
- Декларираш непроменливи и променливи променливи, както и константи, и да обясниш кога подхожда всяка.
- Избираш числови типове, булеви стойности, символи, кортежи, масиви, вектори и низове за прости данни на
revisor. - Пишеш функции с параметри и върнати стойности, без да разчиташ на неявни преобразувания.
- Обясниш защо един блок, един
ifи единloopмогат да произвеждат стойности. - Използваш
if,loop,whileиfor, за да класифицираш и обхождаш данни по четим начин. - Четеш и поправяш два обичайни варианта на грешката
E0308. - Решиш упражненията
variables,functions,ifиprimitive_typesна Rustlings.
Защо, преди как
revisor, който ще изградиш по време на курса, получава списък със услуги, проверява ги и докладва какво е станало. Макар накрая да има HTTP, YAML файлове, конкурентност и JSON изход, ядрото му започва с много по-малки операции: запазване на време за отговор, сравняването му с граница, обхождане на списък и решаване какъв текст да се покаже. Преди да моделираш една услуга със struct или една повреда с enum, трябва да можеш да изразяваш тези операции прецизно.
Помисли за първоначалното правило на програмата: отговор до хиляда милисекунди се счита за нормален; по-бавен се докладва като бавен. Правилото изглежда просто, но съдържа няколко решения, които Rust иска да декларираш: времето не може да бъде текст, трябва да е число; прагът трябва да има съвместим тип; сравнението трябва да даде булево условие; и функцията трябва винаги да връща класификация. Rust не оставя тези решения скрити в автоматични преобразувания или двусмислени стойности. Компилаторът изисква програмата да каже какво представлява всяка данна.
Тази настойчивост може да ти се струва тежка, ако идваш от Go, Python или JavaScript. В Go също има статични типове и преобразуванията между числа са явни, но Rust разширява тази прецизност върху други части на синтаксиса. Променливата е непроменлива по подразбиране. Един блок може да върне стойност. Един if трябва да произвежда стойности от един и същ тип и в двата клона, когато се използва като израз. Един for разграничава обхождане на колекция, заемането ѝ и консумирането ѝ. В началото това са повече видими решения; после са информация, която не позволява на някого да изтълкува погрешно намерението ти, докато поддържа програмата.
Полезният мисловен модел не е „Rust слага препятствия преди изпълнение“. Той е „Rust превръща проектантските решения в неща, които могат да се проверят“. Ако назовеш една мярка като u64, компилаторът знае, че тя не бива да е отрицателна. Ако направиш една променлива mut, читателят знае, че тя ще се променя. Ако една функция връща &'static str, ясно е, че връща един от няколко фиксирани етикета, а не току-що построен текст. Ако резултатът от един if се запазва в променлива, всичките му клонове трябва да описват един и същ тип резултат. По-голямата част от следващото в курса се опира на същата идея.
Този урок работи по глави 2 и 3 на The Rust Programming Language. Глава 2 представя let, функциите и употребата на mut в една малка програма; глава 3 организира основите: променливи, типове данни, функции, коментари и контрол на потока. Не се опитвай да запомниш всички налични типове на един дъх. Важното е да се научиш да четеш една сигнатура, да избираш разумно представяне и да оставяш компилатора да ти посочва противоречията.
Истинската програма вече съдържа тези основи. В programas/revisor/src/modelo.rs има граници, изразени като константи; в src/config.rs има for, който валидира всяка услуга; в src/revisar.rs има условия, които класифицират отговорите; а в src/reporte.rs има променливи променливи за построяване на изхода. Този урок не променя този проект: използва го като карта накъде отиват малките части, които ще упражняваш тук.
Понятията
Променливи, mut, константи и засенчване
Една променлива се декларира с let. По подразбиране обвързването (binding) между името и стойността ѝ е непроменливо: след като напишеш let x = 5;, не можеш да присвоиш друга стойност на x. Този избор е умишлен. Когато четеш дълга функция, всяко име без mut ти дава локална гаранция: това име ще продължи да представлява същата стойност до края на областта си на видимост (scope).
Непроменливостта не означава, че Rust забранява промяната на данни. Означава, че трябва да го декларираш. Ако една променлива представлява брояч, изход, който построяваш постепенно, или индекс, който намалява, използвай let mut. Думата mut стои до името, защото описва обвързването, а не цялата функция. Избягвай да слагаш mut по навик: една променлива с mut, която никога не се променя, предизвиква предупреждение, а компилирането на примерите с -D warnings превръща това предупреждение в грешка. Това е малък, но полезен сигнал: кодът казва, че нещо ще варира, а в действителност това не се случва.
Константите се пишат с const, имат изричен тип и се изчисляват, преди програмата да се изпълни. Използвай ги за правила, чието име трябва да се появява из целия код: времеви лимит, капацитет или максимален брой опити. Константата не е непроменлива променлива с друго име. Тя не заема единствено място в паметта, което можеш да заемеш или промениш; заменя се там, където се използва. На този етап е достатъчно да запомниш практическото правило: let за локални стойности и const за стабилно, именувано правило.
Фиг. 1.1 | Променливи, променливост и константи.
// fig01_01.rs
fn main() {
let x: i32 = 5; // tipo explícito (casi nunca hace falta: lo infiere)
let mut y = 10; // mutable
const MAX: u32 = 100_000; // constante, siempre con tipo
y += x;
println!("x = {x}, y = {y}, MAX = {MAX}");
}
$ rustc --edition 2024 fig01_01.rs && ./fig01_01
x = 5, y = 15, MAX = 100000
Типът на x е записан като i32, но Rust би могъл да го изведе и тук, защото y += x и литералът 10 дават достатъчно контекст. Анотирането на типове помага, когато една сигнатура е част от API, когато компилаторът не може да ги изведе или когато искаш да съобщиш важно ограничение. Не ги анотирай механично във всеки let: добре използваното извеждане намалява шума, без да губи сигурност.
Засенчването (shadowing) е различно от променливостта. При засенчване декларираш нова променлива със същото име; предишната престава да бъде достъпна от този момент нататък. Полезно е, когато една идея минава през етапи и искаш да запазиш честно име. Например текст с интервали и броят на интервалите са две различни стойности, но и двете могат да се казват espacios, защото първата версия вече не е нужна. За разлика от mut, засенчването позволява типът да се промени.
Фиг. 1.2 | Засенчване, кортежи и масиви.
// fig01_02.rs
fn main() {
let espacios = " ";
let espacios = espacios.len();
let medicion: (u16, u64, bool) = (200, 750, true);
let (codigo, ms, saludable) = medicion;
let nombres = ["catalogo", "pagos"];
println!("espacios = {espacios}");
println!("codigo = {codigo}, ms = {ms}, saludable = {saludable}");
println!("primer servicio = {}", nombres[0]);
}
$ rustc --edition 2024 fig01_02.rs && ./fig01_02
espacios = 3
codigo = 200, ms = 750, saludable = true
primer servicio = catalogo
Тук първото espacios е &str, изглед към текст; второто е usize, количество. Не е така, че една променлива е мутирала от текст в число: те са две различни обвързвания с припокриващи се области на видимост. Тази разлика има значение по-нататък при ownership. let mut nombre запазва същата стойност и позволява да се променя; let nombre = ... обвързва името отново и може да трансформира стойността, без да запазва предишната версия.
В revisor променлива с mut се появява при построяването на таблицата, която ще се отпечата. Името salida не представлява фиксирано правило: то е акумулатор, към който се добавят редове, така че mut съобщава точно намерението.
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());
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
}
Още не е нужно да разбираш референции, итератори или format!, за да разпознаеш основното решение: filas и ancho не се променят; salida — да. В урок 2 ще изучиш защо &[Servicio] и &[Estado] са заемания, а в урок 4 ще видиш как String позволява да се построява динамичен текст. Засега разпознай шаблона: декларирай непроменливо, докато една промяна не стане истинска част от работата.
Правилата за класификация на revisor също са константи. Стойността не е повторена като анонимно число във всяко сравнение; има име, тип и коментар. Когато политиката за „бавно“ се промени, ще има очевидно място за преглед.
/// Cuánto se le espera a un servicio que no declara su propio tiempo límite.
const TIMEOUT_POR_OMISION_MS: u64 = 5000;
/// A partir de cuántos milisegundos una respuesta sana se reporta como lenta.
pub const UMBRAL_LENTO_MS: u64 = 1000;
TIMEOUT_POR_OMISION_MS е частна за модула, защото се използва само за създаване на услуги по подразбиране. UMBRAL_LENTO_MS носи pub, защото друг модул, revisar.rs, трябва да я консултира. Видимостта на модулите се изучава формално в урок 6; важното днес е, че и двете стойности имат изрични типове и имена, които изразяват мерните единици. Едно голо 1000 оставя въпроси: хиляда секунди, хиляда байта, хиляда милисекунди? UMBRAL_LENTO_MS им отговаря.
Скаларни и съставни типове
Rust е език със статични типове: преди изпълнение компилаторът знае типа на всяка стойност. Понякога го извежда, а понякога трябва да го анотираш, но никога не третира число като текст и не смесва два целочислени размера, защото „горе-долу изглеждат съвместими“. Тази строгост позволява много грешки да се открият, преди да се произведе бинарен файл.
Скаларните типове съдържат една-единствена стойност. Целите числа със знак са i8, i16, i32, i64, i128 и isize; тези без знак са u8, u16, u32, u64, u128 и usize. Числото казва колко бита заема стойността. isize и usize се променят според архитектурата и се използват главно за размери, дължини и индекси. За количествата милисекунди на revisor u64 е изричен избор: няма отрицателни времена, а диапазонът е широк. За един HTTP код u16 е достатъчен. Изборът на тип не се състои в търсене на възможно най-малкото число; състои се в разумно изразяване на домейна на данната.
i32 е целочисленият тип по подразбиране, когато компилаторът не получава повече контекст. Той е добър общ избор за локални целочислени изчисления. Не допускай, че всички цели числа са i32: един usize, който идва от len(), не може да се събере директно с u64, а един u16 от HTTP код не става i32, само защото е в същата операция. Предимството е, че виждаш пресичането на домейни точно на мястото, където се случва.
Rust не прави неявни числови преобразувания. Това не е изолирана странност: предотвратява едно привидно невинно присвояване да промени размер, знак или диапазон, без този, който е писал кода, да го е обмислил. В Go преобразуванията между числови типове също се искат изрично; Rust запазва тази дисциплина и я прави особено важна, защото целочислените му типове често се използват за представяне на капацитети, дължини и мрежови данни.
Фиг. 1.3 | Няма неявно преобразуване, дори между числа.
// fig01_03.rs
fn main() {
let a: i32 = 5;
let b: i64 = a; // ← no compila
println!("{b}");
}
$ rustc --edition 2024 fig01_03.rs
error[E0308]: mismatched types
--> fig01_03.rs:4:18
|
4 | let b: i64 = a; // ← no compila
| --- ^ expected `i64`, found `i32`
| |
| expected due to this
|
help: you can convert an `i32` to an `i64`
|
4 | let b: i64 = a.into(); // ← no compila
| +++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
За просто и познато преобразуване можеш да използваш as. Преобразуването от i32 в i64 е безопасно в този случай, защото всяко i32 се побира в i64. Въпреки това as позволява и преобразувания, които могат да отрежат, да преинтерпретират знака или да загубят точност. Не го използвай като начин да „накараш компилатора да млъкне“. Когато едно преобразуване може да се провали или да загуби информация, по-нататък ще се запознаеш с TryFrom, TryInto и Result.
Фиг. 1.4 | Преобразуването се иска с as.
// fig01_04.rs
fn main() {
let a: i32 = 5;
let b: i64 = a as i64; // así
println!("{b}");
}
$ rustc --edition 2024 fig01_04.rs && ./fig01_04
5
Освен целите числа, скаларите включват f32 и f64 за числа с плаваща запетая, bool за true или false и char за един символ на Unicode. За revisor избягвай плаващата запетая, ако едно цяло число изразява мерната единица по-добре. Да запазиш 750 милисекунди като u64 е по-ясно, отколкото да запазиш 0.75 секунди като f64, и избягва въпросите за закръгляне, когато показваш, сравняваш или сериализираш стойността.
Съставните типове групират няколко стойности. Един кортеж (tuple) може да съдържа елементи от различни типове и има фиксиран размер. На фигура 1.2 (u16, u64, bool) представлява три резултата, които принадлежат на едно и също измерване: код, продължителност и състояние на здравето. Деструктурирането let (codigo, ms, saludable) = medicion; извлича тези стойности с полезни имена. Кортежите са подходящи за малки и локални резултати; когато значението на полетата е централно за програмата, както ще бъде при една услуга, по-добре е структура с именувани полета. Това идва в урок 3.
Един масив (array) като ["catalogo", "pagos"] съдържа стойности от един и същ тип и има фиксирана дължина, позната по време на компилация. Един вектор, Vec<T>, също съдържа стойности от един и същ тип, но може да расте или да се свива по време на изпълнение. Фигура 1.7 използва vec!, защото списъкът със услуги е колекция, която по понятие може да променя размера си. В истинския проект конфигурацията се зарежда от YAML и произвежда Vec<Servicio> по същата причина.
Низовете също изискват прецизност. Един литерал като "catalogo" обикновено е &str, заето виждане към вече съществуващ текст. Един String е текст, който притежава памет и може да расте. В този урок ще видиш &str като изходна стойност за фиксирани етикети; в урок 2 ще изучиш защо не всички текстове могат да се копират и защо двата типа се различават. Засега запомни това правило: фиксиран текст, написан в кода, обикновено е &str; текст, прочетен, построен или съхранен, обикновено завършва като String.
revisor прави речника си за числата явен. Времевият лимит се съхранява като u64, а HTTP кодът като Option<u16>. Не е нужно още да владееш Option; урок 3 ще обясни защо той замества nil. Днес е достатъчно да забележиш, че типът описва ограничение на реалността: може да няма HTTP код, ако не е дошъл отговор.
#[derive(Serialize)]
pub struct EstadoJson {
pub servicio: String,
pub codigo: Option<u16>,
pub ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
Типът не е декоративна документация. ms: u64 не позволява да му присвоиш низ; codigo: Option<u16> не позволява да третираш липсата на отговор като автоматично 200; servicio: String показва, че името е текст, който програмата притежава. Rust ще използва тази информация по време на цялата компилация.
Функции, параметри и върнати стойности
Една функция се декларира с fn, има име, параметри в скоби и тяло във фигурни скоби. Параметрите винаги носят тип: fn doble(x: i32) казва както името на данната, така и какво може да получи функцията. Ако връща нещо различно от (), това се посочва след стрелка: -> i32. Тази сигнатура е кратък и проверим договор. Който извиква функцията, знае какво трябва да предаде и какво ще получи; компилаторът проверява и двата края.
За разлика от Go, Rust пише типа след името на параметъра, а не преди него. В Go би написал func doble(x int) int; в Rust — fn doble(x: i32) -> i32. Визуалната разлика престава да има значение след няколко функции. Важното е, че и в двата езика сигнатурата е част от дизайна: не е коментар, нито неформална конвенция.
Една малка функция не бива да поема работа, която не ѝ принадлежи. doble получава едно число и връща друго; не печата, не чете файлове и не променя външно състояние. Това разделяне изглежда елементарно, но подготвя почвата за revisor: функция, която класифицира милисекунди, може да се тества с три числа, без да се стартира HTTP клиент или да се отваря конфигурация. Когато програмата расте, разделянето на логиката на функции с ясни входове и изходи ще бъде начин да се запази разбираема.
Фиг. 1.5 | Всичко е израз.
// fig01_05.rs
fn main() {
let x = 7;
let n = if x > 5 { "grande" } else { "chico" }; // el if DEVUELVE valor
let cuadrado = {
let t = x * x;
t // 🔑 sin punto y coma = es el valor del bloque
};
println!("{n} {cuadrado} {}", doble(x));
}
fn doble(x: i32) -> i32 {
x * 2 // sin `return` y sin `;`
}
$ rustc --edition 2024 fig01_05.rs && ./fig01_05
grande 49 14
main също е функция. В една изпълнима програма започва без параметри и не е нужно да декларира връщане, ако само завършва. Напротив, doble обещава i32, така че последната стойност на тялото ѝ трябва да е съвместима с i32. Можеш да използваш return x * 2;, но това не е обичайната форма за последната стойност на една функция. Rust предпочита крайния израз, защото оставя видимо какъв резултат произвежда тялото.
Параметрите се предават по различни начини според типа и намерението. Скаларните типове като i32, u64 и bool се копират евтино; да получиш ms: u64 не пречи на извикващия да продължи да използва своята мярка. С String, вектори и по-сложни структури ще се появят правилата за преместване и заемане от урок 2. Не ги изпреварвай, като решаваш всичко с копия. Засега използвай скаларни параметри, за да упражняваш чисти сигнатури, и разбери, че &str на фиксиран етикет има различен живот от String, който се построява.
Истинският проект има една малка функция, която превръща текстова конфигурация в данни на програмата. Макар да използва библиотеки, които ще изучиш по-късно, сигнатурата ѝ показва съществения шаблон: получава вход, връща резултат и тялото ѝ завършва с израз Ok(...).
pub fn cargar(ruta: &str) -> Result<Vec<Servicio>> {
// with_context agrega a qué archivo se refería el error, como el %w de Go
let txt = std::fs::read_to_string(ruta).with_context(|| format!("leyendo {ruta}"))?;
Ok(yaml_serde::from_str(&txt)?)
}
Още не е нужно да разчленяваш Result, ? или yaml_serde; те идват в урок 4. Това, което вече можеш да прочетеш, е формата: ruta влиза като изглед към текст, функцията обещава да върне списък със услуги или грешка, txt е непроменлива локална стойност, а Ok(...) е крайният резултат. Сигнатурите ти позволяват да разбереш границата на една функция още преди да познаваш всичките ѝ вътрешни подробности.
Изрази, оператори и точката и запетаята
В Rust много конструкции произвеждат стойност. Една аритметична операция като x * 2 произвежда число; един блок във фигурни скоби може да произведе последната стойност, която съдържа; един if може да произведе една от две стойности; а един loop може да завърши със стойност, изпратена от break. Тези конструкции се наричат изрази.
Един оператор (statement) извършва действие, но не произвежда полезна стойност. Декларацията let x = 7; е оператор. Такъв е и един израз, към който добавиш точка и запетая. Стойността на един оператор е (), наречен unit тип. Можеш да мислиш за () като за „няма резултат за предаване“. Не е грешка, нито нулева стойност: това е истински тип, който се появява, когато една операция се използва само заради ефекта си.
Точката и запетаята определя тази разлика на важни места. На фигура 1.5 блокът, присвоен на cuadrado, завършва с t без точка и запетая, затова блокът произвежда стойността на t. Функцията doble завършва с x * 2 без точка и запетая, затова връща това i32. Ако добавиш ;, операцията се изпълнява и резултатът ѝ се изхвърля. Тогава тялото на функцията произвежда (), а сигнатурата изисква i32.
Фиг. 1.6 | Излишната точка и запетая.
// fig01_06.rs
fn doble(x: i32) -> i32 {
x * 2; // ← el punto y coma de más
}
fn main() {
println!("{}", doble(4));
}
$ rustc --edition 2024 fig01_06.rs
error[E0308]: mismatched types
--> fig01_06.rs:2:21
|
2 | fn doble(x: i32) -> i32 {
| ----- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
3 | x * 2; // ← el punto y coma de más
| - help: remove this semicolon to return this value
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
Тази грешка е объркваща веднъж и много полезна после. Компилаторът не казва, че умножението е невалидно; казва, че функцията е обещала i32 и е завършила, като произвежда (). Прочети двете части на съобщението: expected i32, found () идентифицира противоречието, а помощта предлага да се махне точката и запетаята. Не добавяй return, без да разбираш защо; в този случай проблемът е, че си изхвърлил правилната стойност.
Стилът на изразите прави малките трансформации компактни и ясни. Можеш да изчислиш междинна стойност в един блок, да запазиш локалните променливи вътре в този блок и да предадеш само резултата. Това намалява ненужните области на видимост и избягва временни имена, които остават живи, когато вече не значат нищо. Не превръщай всеки ред в сложен израз: четимостта си остава критерият. Блок с две-три добре наименувани стъпки обикновено е по-ясен от един хитроумен ред.
В revisar.rs класификацията на един отговор използва условия във match; резултатът от всеки клон е Estado. Макар match да се изучава подробно в урок 3, шаблонът вече е познат: всеки път произвежда стойността, която функцията е обещала.
match respuesta {
Ok(r) if r.status().is_success() && ms > UMBRAL_LENTO_MS => Estado::Lento {
codigo: r.status().as_u16(),
ms,
},
Ok(r) if r.status().is_success() => Estado::Ok {
codigo: r.status().as_u16(),
ms,
},
Ok(r) => Estado::Falla {
motivo: format!("codigo {}", r.status().as_u16()),
ms,
},
Err(e) if e.is_timeout() => Estado::Falla {
motivo: "se acabo el tiempo de espera".to_string(),
ms,
},
Err(_) => Estado::Falla {
motivo: "no responde".to_string(),
ms,
},
}
Частта, която съответства на този урок, са условията if и идеята, че една конструкция за контрол завършва със стойност. Новата част са match, Result и вариантите на Estado. Не е нужно още да ги копираш; само разпознай, че синтаксисът, който упражняваш с числа, ще свърши с вземането на истински решения за услуги.
if, loop, while и for
if оценява условие, което трябва да е bool. Rust не смята, че 0, празен низ или нулева референция са неверни автоматично. Напиши сравнение или използвай булева променлива. Това решение избягва случайните условия и прави очевидно кое свойство питаш: ms > UMBRAL_LENTO_MS съобщава правило; if ms не би имало смисъл.
Когато if се използва за избор на стойност, клоновете му трябва да върнат един и същ тип. Не можеш да върнеш "rápido" в един клон и 1000 в друг, защото променливата, която получава резултата, трябва да има последователно представяне. Това ограничение е точно видът решение, което става полезно в един доклад: една класификация винаги ще е текст, един изходен код винаги ще е подходящо цяло число, а едно състояние винаги ще е вариант на един и същ тип.
loop започва безкраен цикъл. Изглежда екстремен инструмент, но е подходящ, когато не знаеш предварително броя на итерациите и естественият изход е break. За разлика от други езици, break valor може да даде резултата на един loop. Това е полезно, когато цикълът търси или изчислява нещо; намерената стойност излиза директно като резултат от израза.
while condicion повтаря, докато условието е вярно. Използвай го, когато напредъкът зависи от състояние, което контролираш: намаляване на брояч, четене до някакво условие или повтаряне при изрично правило. Увери се, че тялото може да промени състоянието, което прави условието невярно. Един while, чийто брояч никога не се обновява, е безкраен цикъл в маскировка.
for е нормалният избор за обхождане на колекция или диапазон. Rust няма традиционния стил for инициализация; условие; обновяване на C, Java или Go. Вместо това обхожда нещо, което умее да предава елементите си един по един (в Rust — нещо, което имплементира IntoIterator): диапазон като 0..10, списък, масив или итератор. Тази форма премахва голяма част от кода с индекси и намалява грешките в границите.
Фиг. 1.7 | Циклите.
// fig01_07.rs
fn main() {
let mut x = 3;
let servicios = vec!["catalogo", "pagos", "reportes"];
loop { break; } // infinito, con break
while x > 0 { x -= 1; }
for i in 0..10 { print!("{i} "); } // rango: 0 a 9
println!();
for i in 0..=10 { print!("{i} "); } // inclusivo: 0 a 10
println!();
for s in &servicios { println!("{s}"); } // sobre una referencia, para no consumir la lista
let r = loop { break 42; }; // 🔑 loop devuelve valor con break
println!("x = {x}, r = {r}");
}
$ rustc --edition 2024 fig01_07.rs && ./fig01_07
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 10
catalogo
pagos
reportes
x = 0, r = 42
Диапазоните са честа причина за грешки в границите. 0..10 включва 0 и изключва 10, затова има десет стойности: от нула до девет. 0..=10 включва и двата края и има единадесет стойности. За да обходиш позициите на масив с дължина десет, почти винаги искаш 0..10 или, още по-добре, да обходиш директно елементите. Използвай включващия диапазон само когато крайната граница е част от правилото и трябва да се появи.
Редът for s in &servicios носи референция към списъка. Това позволява да се чете всеки елемент, без да се предава собствеността върху servicios. Пълната разлика между servicios, &servicios и &mut servicios е темата на урок 2, но още от днес можеш да възприемеш едно временно правило: ако искаш само да разгледаш една колекция и да я запазиш, обхождай я по референция. Компилаторът ще предотврати опасните употреби, когато познаваш правилата за заемане.
revisor валидира списък с for. Функцията не трябва да знае колко услуги са пристигнали: взема ги една по една. enumerate() добавя индекса, за да може текущата услуга да се сравни с предишните. Макар целият израз да изглежда напреднал, потокът му е същият като на фигура 1.7: обхожда, проверява условие и завършва с резултат.
pub fn validar(servicios: &[Servicio]) -> Result<()> {
anyhow::ensure!(
!servicios.is_empty(),
"el archivo no declara ningún servicio"
);
for (i, s) in servicios.iter().enumerate() {
anyhow::ensure!(
!servicios[..i].iter().any(|antes| antes.nombre == s.nombre),
"el nombre «{}» está repetido",
s.nombre
);
anyhow::ensure!(
s.url.starts_with("http://") || s.url.starts_with("https://"),
"la URL «{}» de «{}» debe empezar con http:// o https://",
s.url,
s.nombre
);
anyhow::ensure!(
s.timeout_ms > 0,
"el tiempo límite de «{}» debe ser mayor que cero",
s.nombre
);
}
Ok(())
}
Тук s.timeout_ms > 0 е булево условие като тези, които вече си използвал. Разликата е, че вместо да печата етикет, anyhow::ensure! спира валидацията с грешка, ако условието е невярно. Урок 4 ще обясни Result и този вид обработка на грешки. Урок 2 ще обясни референциите на &[Servicio]. Вече можеш да прочетеш намерението, без да познаваш всяка подробност: всички услуги трябва да имат URL със схема, неповтарящо се име и лимит, по-голям от нула.
Грешката, която ще видиш
E0308: типове, които не съвпадат
E0308 означава, че Rust е очаквал един тип на дадено място в програмата и е намерил друг. Това не е неясно съобщение: чети го като изречение с две части. Първо определи мястото, където е фиксирано очакването; после определи стойността, която противоречи на това очакване. На фигура 1.3 анотацията let b: i64 фиксира, че b ще бъде i64; променливата a е i32; затова присвояването се проваля.
Поправката не винаги ще бъде as. За да преобразуваш от i32 в i64, разширяването е безопасно и as i64 съобщава намерението. За да преобразуваш от голямо число в малко или от текст в цяло число, трябва да решиш какво да правиш, когато стойността не се побира или няма валиден формат. Тези преобразувания ще се разглеждат с резултати, които могат да се провалят. Добрата практика е да се разреши несъответствието на границата между домейните, а не да се преобразуват стойностите многократно във всяка функция.
Фигура 1.6 произвежда същия код E0308, но по друга причина: функцията декларира -> i32, а последният ѝ елемент е оператор, чиято стойност е (). Тази разлика илюстрира защо не бива да решаваш грешките само по номера. Кодът групира семейство диагностики; посочените редове и думите expected и found разказват конкретната история.
Когато видиш expected i32, found (), задай тези въпроси: функцията обещала ли е връщане? последната стойност има ли точка и запетая? някой клон на if не връща ли същото като другия? не съм ли сложил println! като последен елемент, когато ми трябваше да произведа стойност? С този ред обикновено откриваш проблема, без да търсиш отговори наслуки.
Как да прочетеш предложение, без да му се подчиняваш сляпо
Rust обикновено предлага секция help:. Това е контекстно предложение, не заповед. На фигура 1.3 предлага a.into(), което също може да преобразува стойността, защото съществува познато преобразуване между двата типа. Фигура 1.4 запазва as i64, защото това е формата, която се иска да се преподава за изрично и просто числово преобразуване. В други случаи предложението може да е clone(), добавяне на референция или промяна на сигнатура. Преди да го приемеш, попитай се каква цена, собственост или поведение въвежда.
Съобщението завършва и с rustc --explain E0308. Тази команда отваря общо обяснение на кода на грешката, инсталирано с твоя компилатор. Използвай я, когато локалната диагностика не стига, но започни от файла, реда и колоните, които компилаторът вече ти е показал. Почти винаги съдържат повече конкретна информация за твоята програма от едно общо търсене.
Какво се прави погрешно
Декларирането на всичко като mut
Да декларираш всяка променлива с mut, за да „имаш свобода“, заличава информация. Ако едно име не се променя, читателят не бива да трябва да проследява цялата функция, за да го разбере. Освен това компилаторът предупреждава, когато mut не е нужен. Декларирай като променливо единствено това, което алгоритъмът променя, като x при броене назад или salida при построяване на доклад.
Използването на as, за да се заглушат грешки в типовете
Едно преобразуване с as може да е правилно, но не е универсално лекарство. Преобразуването на голямо u64 в u16 може да загуби данни; преобразуването на цяло число със знак в такова без знак може да даде изненадваща стойност. Определи какво представлява всяко число и преобразувай веднъж, на границата, където променяш домейна. Ако преобразуването може да се провали, програмата трябва да го изрази, вместо да го крие.
Използването на числа без мерни единици и без име
Един if ms > 1000 работи, но принуждава да помниш какво представлява 1000. Милисекунди ли са, секунди или байтове? Използвай константа като UMBRAL_LENTO_MS, когато стойността е бизнес правило. За очевидни локални стойности един литерал може да е добре; за политика, която ще се повтаря или променя, едно име избягва грешки и подобрява четенето.
Добавянето на точка и запетая към последния израз по рефлекс
В много езици всеки ред завършва с точка и запетая или конвенцията те подканва да я използваш. В Rust последната точка и запетая на една функция или блок променя стойността му на (). Не запомняй изключение; разпознай правилото: краен израз без точка и запетая може да е резултатът. Ако един блок съществува, за да изчисли нещо, провери какво оставя като последен израз.
Писането на for i in 0..lista.len(), когато ти трябват само елементите
Обхождането на индекси работи, но добавя ненужен начин да сгрешиш. Ако ти трябва само всяка услуга, напиши for servicio in &servicios. Използвай enumerate(), когато индексът е истинска част от логиката, както във валидацията на revisor. Използвай директни индекси, когато трябва да достъпваш конкретни позиции и можеш да обосновеш границите.
Използването на loop, когато броят на стъпките вече е известен
Един loop с няколко условия за изход може да е правилен, но ако имаш колекция или известен диапазон, for изразява намерението по-добре. Ако зависи от променящо се условие, while обикновено показва критерия за край по-ясно. Запази loop за процеси, които наистина очакват изход чрез break, като четец на събития или търсене, което свършва, когато намери данната.
Упражнения
Упражнение 1 — Класифицирай един отговор
Напиши fn clasificar(ms: u64) -> &'static str. Тя трябва да върне "rápido", ако времето е по-малко или равно на 1000, "lento", ако е по-голямо от 1000 и по-малко или равно на 5000, и "timeout", ако е по-голямо. Използвай if като израз: не използвай return. От main отпечатай класификацията на 700, 1500 и 6000, по един на ред.
Преди да видиш решението, провери, че трите клона връщат един и същ тип. Сигнатурата не трябва да създава String: трите етикета са фиксирани литерали и затова могат да са &'static str.
Упражнение 2 — Сумиране, без да се консумира списъкът
Напиши fn sumar(valores: &[i32]) -> i32, която използва for, за да сумира един slice. От main създай let valores = vec![3, 5, 8];, отпечатай резултата и после отпечатай дължината на valores. Второто отпечатване трябва да се компилира: показва, че обхождането не е консумирало вектора.
Направи само акумулатора променлив. Не прави вектора променлив: не добавяш, не премахваш и не променяш елементите му.
Упражнение 3 — Минимален доклад на revisor
Декларирай const UMBRAL_LENTO_MS: u64 = 1000; и напиши fn etiqueta(ms: u64) -> &'static str, която връща "OK" до прага и "LENTO" над него. В main използвай масив с [120_u64, 1000, 1500] и for, за да отпечаташ точно тези редове:
120ms: OK
1000ms: OK
1500ms: LENTO
После промени типа на масива на i32, без да променяш сигнатурата на etiqueta. Прочети E0308, поправи го по изричен начин и обясни със свои думи защо Rust не направи преобразуването вместо теб.
Решения
Решение 1
fn clasificar(ms: u64) -> &'static str {
if ms <= 1000 {
"rápido"
} else if ms <= 5000 {
"lento"
} else {
"timeout"
}
}
Целият if е крайният израз на функцията. Всеки клон връща литерал от тип &'static str, така че сигнатурата и резултатът съвпадат. Редът има значение: второто условие се оценява само ако първото е било невярно, затова не е нужно да се повтаря ms > 1000.
Решение 2
fn sumar(valores: &[i32]) -> i32 {
let mut total = 0;
for valor in valores {
total += valor;
}
total
}
valores получава референция към slice, така че функцията наблюдава числата, без да задържа вектора на извикващия. Вътре във for valor е референция към всяко i32; сумирането работи, защото i32 имплементира събирането с референция към друго i32 (total += valor), без да се налага да пишеш *valor. Последният израз, total, предава резултата без точка и запетая.
Решение 3
const UMBRAL_LENTO_MS: u64 = 1000;
fn etiqueta(ms: u64) -> &'static str {
if ms <= UMBRAL_LENTO_MS {
"OK"
} else {
"LENTO"
}
}
fn main() {
let mediciones = [120_u64, 1000, 1500];
for ms in mediciones {
println!("{ms}ms: {}", etiqueta(ms));
}
}
Суфиксът _u64 на първия литерал фиксира типа на масива. Останалите елементи трябва да са от същия тип, така че Rust ги тълкува също като u64. Константата изразява както стойността, така и мерната единица на правилото. Ако смениш масива на i32, трябва да преобразуваш всяка данна изрично или да промениш договора на функцията; и двете решения имат значение и не бива да се случват случайно.
Как да разбера, че съм успял
- От
programas/01-fundamentos,rustc --edition 2024 -D warnings fig01_01.rs && ./fig01_01отпечатваx = 5, y = 15, MAX = 100000без предупреждения. rustc --edition 2024 fig01_03.rsсе проваля сerror[E0308]и можеш да посочиш, чеbочакваi64, докатоaеi32.rustc --edition 2024 fig01_06.rsсе проваля сerror[E0308]и можеш да обясниш, че точката и запетаята е накарала функцията да върне().rustc --edition 2024 -D warnings fig01_07.rs && ./fig01_07отпечатва двата диапазона, трите услуги и завършва сx = 0, r = 42.rustc --edition 2024 -D warnings fig01_02.rs && ./fig01_02отпечатва трите документирани резултата и не докладва предупреждения.- Завърши секциите
variables,functions,ifиprimitive_typesна Rustlings и можеш да решиш трите упражнения, без да копираш решенията.
За по-нататъшно четене
- The Rust Programming Language, глава 2: Programming a Guessing Game — консултирано на 2 октомври 2026 г.
- The Rust Programming Language, глава 3: Common Programming Concepts — консултирано на 2 октомври 2026 г.
- Официална документация на
i32и на примитивните числови типове — консултирано на 2 октомври 2026 г. - Rustlings — завърши
variables,functions,ifиprimitive_types; консултирано на 2 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx