Съдържание на курса
Урок 3 — Structs, enums и match
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 2 × 45 мин.
Какво изграждаш: моделът на revisor: Servicio и Estado
Какво научаваш: struct-ове и impl, enum-и, които носят данни, изчерпателен match, Option вместо nil
The Rust Book, глави 5 и 6. Rustlings: structs, enums, options.
След края ще можеш да
- Моделираш една услуга със
struct, чиито полета имат име и тип. - Пишеш методи в блок
implи да решаваш дали получават&self,&mut selfилиself. - Представяш взаимно несъвместими резултати с
enum, който носи данни. - Напишеш изчерпателен
match, който извлича данни от всеки вариант. - Обясниш защо добавянето на нов вариант те принуждава да преразгледаш вече взети решения.
- Използваш
Option<T>, когато една стойност може да липсва, без да прибягваш доnil. - Избираш между
match,if letи методи катоunwrap_orспоред намерението на кода.
Защо, преди как
До тук курсът е използвал прости стойности: числа за времена, низове за имена и условия за класифициране на един отговор. Това стига, за да упражняваш променливи, функции, типове и ownership, но не стига, за да опишеш реалния домейн на revisor. Една услуга не е просто име, URL адрес и времево ограничение, които случайно се появяват заедно в три променливи. Това са три данни, които описват една-единствена вещ и които трябва да пътуват, да се валидират и да се консултират като едно цяло.
Съхраняването на тези данни поотделно поражда мълчаливи грешки. Представи си, че имаш nombre_catalogo, url_catalogo, timeout_catalogo, после добавяш същите три стойности за плащанията и за отчетите и когато строиш доклада, съчетаваш името на плащанията с URL адреса на каталога. Компилаторът не може да открие проблема: трите части са с валидни типове, но връзката помежду им е изгубена. struct позволява да декларираш тази връзка веднъж и да я превърнеш в част от типа.
Вторият проблем се появява след като си питал една услуга. Здравият отговор носи HTTP код и продължителност. Бавният отговор също носи и двете данни, но изисква друг етикет. Една повреда може да носи съобщение и продължителност, но не непременно HTTP код. А услуга, която още не е питана, няма нито код, нито продължителност, нито съобщение за повреда. Ако се опиташ да запазиш всичко това в един-единствен struct с полета, които са „понякога валидни“, ще получиш абсурдни комбинации: състояние на повреда с код 200, непопитана услуга с продължителност 0 ms, която никой не знае как да тълкува, или празно съобщение за грешка, което означава различни неща според друго булево поле.
Rust решава това моделиране с enum. За разлика от традиционния enum на други езици, който обикновено е списък от числа или именувани константи, един вариант в Rust може да носи данни. Estado::Ok носи код и милисекунди; Estado::Falla носи причина; Estado::NoIntentado не носи нищо, защото няма честни данни за запазване. Типът изразява, че една стойност е точно в едно от тези състояния, никога в няколко едновременно.
Третата част е match. Когато получиш един Estado, не стига да знаеш, че принадлежи на enum-а: трябва да решиш какво да правиш с всяка възможност. Rust изисква това решение да покрива всички варианти. Това не е препоръка за стил, нито правило на linter: то е част от компилацията. Ако утре добавиш Estado::Rechazado, всеки match, който преди изглеждаше завършен, се превръща в място, което компилаторът ти посочва за преглед. Това задължение е предпазна мрежа при рефакториране.
Курсът по Go изгражда същия revisor, но тук се появява важна разлика между двата езика. В Go един резултат обикновено се моделира със struct, полета с нулева стойност, указатели и конвенции за това кои полета присъстват. В Rust типът може директно да представя несъвместими алтернативи. Това не премахва нуждата да мислиш за домейна, но прави правилните решения по-лесни за изразяване, а противоречията — по-трудни за компилиране.
Последната част от модела е отсъствието. В много езици една референция може да е null или nil, макар типът ѝ да не го казва видимо. Програмата стига до ред, който е очаквал обект, получава отсъствие и се проваля по време на изпълнение. Rust няма nil. Когато нещо може да липсва, типът му го декларира чрез Option<T>. Това те принуждава да вземеш решение, преди да използваш съдържанието: да обработиш Some(valor), да обработиш None или да предоставиш изрична алтернатива. Отсъствието престава да е скрита случайност и става част от договора на функцията.
Този урок не е за това да запаметиш целия синтаксис на шаблоните. Той е за това да се научиш да се питаш кои реални състояния съществуват, кои данни принадлежат на всяко състояние и кои решения трябва да се променят, когато моделът се промени. Тези въпроси се връщат в урок 4 с Result, в 5 с trait-ове, в 6 с тестове и в 8, когато revisor генерира JSON.
Понятията
Struct-ове: име за данни, които принадлежат заедно
struct дефинира съставен тип с именувани полета. Важната дума е „тип“: след като дефинираш Servicio, Rust престава да вижда неформална колекция от три данни и започва да вижда стойност, която представя една услуга. Всяко поле запазва собствения си тип, така че компилаторът продължава да различава текст от числа, но вече знае и че тези стойности образуват един-единствен обект.
Struct с именувани полета е добър избор, когато всяка позиция има собствено значение. Кортеж като (String, String, u64) може да съхрани име, URL и времево ограничение, но те принуждава да помниш какво означават .0, .1 и .2. С Servicio кодът казва servicio.timeout_ms, което съобщава както данната, така и нейната мерна единица. Яснотата не е украса: намалява възможността да размениш сходни стойности и прави по-лесно четенето на код, който си написал преди седмици.
Създаването на struct използва фигурни скоби и двойки campo: valor. Достъпът също е директен с точка. Тъй като полетата от фигурата принадлежат на Servicio, няма временно състояние, в което да съществува URL без име или времево ограничение, случайно свързано с друга услуга. Все още е възможно да се създаде неправилна стойност, например URL без схема; урок 4 ще те научи как да я валидираш. Изчезва безредието от разпилени променливи.
Фиг. 3.1 | Struct с неговите методи.
// fig03_01.rs
#[derive(Debug, Clone)] // el compilador te escribe esos comportamientos
struct Servicio {
nombre: String,
url: String,
timeout_ms: u64,
}
impl Servicio {
fn new(nombre: &str, url: &str) -> Self { // no hay constructores: es convención
Self { nombre: nombre.to_string(), url: url.to_string(), timeout_ms: 5000 }
}
fn etiqueta(&self) -> String { // &self = presta, no consume
format!("{} ({})", self.nombre, self.url)
}
}
fn main() {
let s = Servicio::new("catalogo", "http://localhost:8090/ok");
println!("{}", s.etiqueta());
println!("{:?}", s.clone());
println!("timeout: {} ms", s.timeout_ms);
}
$ rustc --edition 2024 fig03_01.rs && ./fig03_01
catalogo (http://localhost:8090/ok)
Servicio { nombre: "catalogo", url: "http://localhost:8090/ok", timeout_ms: 5000 }
timeout: 5000 ms
#[derive(Debug, Clone)] моли компилатора да имплементира известни поведения за типа. Debug позволява да се отпечата полезно представяне с {:?}. Това не е стабилен формат за крайни потребители: това е изглед за разработка и диагностика. Clone позволява да поискаш изрично копие с .clone(). В този пример се използва само за да се покаже, че структурата може да се отпечата и след това да остане достъпна; не бива да копираш стойности по навик, за да заглушаваш грешки на ownership.
revisor използва същия модел, но добавя атрибутите, нужни за четене на услуги от YAML. pub показва, че други модули на crate-а могат да достъпват тези полета. Deserialize и serde ще се появят подробно в следващите уроци; засега забележи, че ядрото си остава същото: име, URL и времево ограничение.
#[derive(Debug, Clone, Deserialize)]
pub struct Servicio {
pub nombre: String,
pub url: String,
#[serde(default = "timeout_por_omision")] // si falta en el YAML
pub timeout_ms: u64,
}
Атрибутът #[serde(default = "timeout_por_omision")] не променя какво е една услуга. Той описва правило за входа: ако YAML не декларира timeout_ms, програмата използва пет секунди. Важно е да различаваш моделирането от валидирането. Struct-ът декларира кои данни образуват една услуга; правилата за това дали един URL има схема, дали времето е по-голямо от нула или дали името се повтаря се проверяват по-късно, когато програмата получи списък.
impl и методи: поведение, което принадлежи на типа
Един struct съхранява данни, но типът може да има и операции, които имат смисъл за тези данни. Rust групира тези операции в блок impl. Името означава implementation: имплементация на поведение за един тип. Няма специална запазена дума за конструктори. По конвенция една асоциирана функция, наречена new, създава нова стойност, но си остава нормална функция в impl.
Фигурата използва два начина за извикване на функции в един impl. Servicio::new(...) използва ::, защото new още няма инстанция, върху която да работи. s.etiqueta() използва ., защото etiqueta получава конкретна услуга. Rust позволява този синтаксис на метод, когато първият параметър се казва self, &self или &mut self.
&self означава „заеми тази стойност за четене“. Методът може да гледа nombre и url, да построи нов низ и да го върне, но не взема собствеността на s и не променя полетата му. Затова след s.etiqueta() пак можеш да отпечаташ s, да прочетеш s.timeout_ms или да заемеш услугата на друга функция. Това е пряко приложение на заеманията от урок 2 към функция, която живее до своя тип.
&mut self означава „заеми тази стойност, за да я променяш“. Метод като fn cambiar_timeout(&mut self, ms: u64) би изисквал този, който го извиква, да декларира променлива с mut и не би позволил други активни референции към същата стойност. Компилаторът прилага същите правила, които вече видя с &mut String: само една променлива референция в даден момент или няколко непроменливи, но не и двата вида едновременно.
self без & консумира стойността. Това е умишлено и по-рядко решение. Полезно е, когато методът трансформира една стойност в друга и оригиналът вече не бива да съществува, например операция, която превръща временна конфигурация във валидирана структура. Не е по-бърз начин да се напише &self: променя кой притежава стойността. Ако получиш грешка „стойност е преместена“ след извикване на метод, първо провери получателя му (receiver).
В реалния проект Servicio::new концентрира стойността по подразбиране. Това избягва всяко извикване да повтаря 5000 и намалява риска някои услуги да бъдат създадени без умисъл с различно правило.
impl Servicio {
/// Un servicio con el tiempo límite por omisión (5 segundos).
pub fn new(nombre: &str, url: &str) -> Self {
Self {
nombre: nombre.to_string(),
url: url.to_string(),
timeout_ms: TIMEOUT_POR_OMISION_MS,
}
}
}
Self в impl Servicio означава Servicio. Използването му избягва повтарянето на името на типа и запазва намерението, ако типът смени името си при рефакториране. Изразът Self { ... } строи стойността; -> Self декларира типа, който връща функцията. Константата TIMEOUT_POR_OMISION_MS е извън откъса и не позволява числото пет хиляди да остане пръснато из програмата като магическа стойност.
Enum-и с данни: валидни алтернативи, не двусмислени полета
enum описва стойност, която може да приеме един от няколко варианта. Същественото различие от колекция от константи е, че всеки вариант може да има собствена форма. Estado::Ok и Estado::Lento имат именувани полета codigo и ms. Estado::Falla пази низ. Estado::NoIntentado не носи данни, защото не е имало запитване, което да даде честни резултати.
Този дизайн избягва да се представя една повреда като специален код, като 0, -1 или празен низ. Такива маркери те принуждават да помниш правила извън типа: „ако кодът е нула, чети грешката; ако грешката е празна, може би е било успешно; ако времето е нула, може би не е опитвано“. Един enum прехвърля тези правила на компилатора. Ако имаш Estado::Falla, Rust знае, че има съобщение; ако имаш Estado::Ok, Rust знае, че има код и продължителност.
Избягва и невъзможната комбинация от незадължителни полета. Struct като Resultado { codigo: Option<u16>, error: Option<String>, ms: u64 } по построение допуска както codigo: Some(200), error: Some("no responde"), така и codigo: None, error: None. Може да има законни случаи за такава форма, особено при сериализиране на външни данни, но не е добро вътрешно представяне на взаимно изключващи се алтернативи. За състоянието на едно запитване enum-ът изразява по-добре реалността.
enum Estado {
Ok { codigo: u16, ms: u64 },
Lento { codigo: u16, ms: u64 },
Falla(String),
NoIntentado,
}
Синтаксисът има три форми, които си струва да разпознаваш. Вариантите с фигурни скоби приличат на малки struct-ове и позволяват да се именуват полетата при създаване и при деструктуриране. Вариантите с обикновени скоби приличат на кортежи и служат, когато данната има ясно основно значение, като съобщението на една повреда в този фрагмент. Вариантите без данни представят възможност, която не се нуждае от допълнителна информация.
Фиг. 3.2 | Enum с данни и неговият match.
// fig03_02.rs
#[allow(dead_code)] // este ejemplo no lee el código de los lentos
enum Estado {
Ok { codigo: u16, ms: u64 },
Lento { codigo: u16, ms: u64 },
Falla(String), // lleva el mensaje dentro
NoIntentado,
}
fn main() {
let estados = [
Estado::Ok { codigo: 200, ms: 120 },
Estado::Lento { codigo: 200, ms: 1800 },
Estado::Falla("no responde".to_string()),
Estado::NoIntentado,
];
for estado in estados {
let texto = match estado {
Estado::Ok { codigo, ms } => format!("OK {codigo} en {ms}ms"),
Estado::Lento { ms, .. } => format!("LENTO {ms}ms"),
Estado::Falla(msg) => format!("FALLA: {msg}"),
Estado::NoIntentado => "sin revisar".to_string(),
};
println!("{texto}");
}
}
$ rustc --edition 2024 fig03_02.rs && ./fig03_02
OK 200 en 120ms
LENTO 1800ms
FALLA: no responde
sin revisar
Анотацията #[allow(dead_code)] принадлежи на примера, не е рецепта за скриване на предупреждения в реални проекти. Вариантът Lento запазва codigo, защото един бавен отговор може да е бил HTTP 200, макар че тази програма използва само ms. Без анотацията Rust би предупредил, че полето codigo на този вариант не се чете в този файл. Реалният проект използва данните там, където е нужно, и се компилира с предупреждения, третирани като грешки.
revisor подобрява първоначалния фрагмент с две решения от домейна. Първо, една повреда носи едновременно motivo и ms, защото да знаеш, че една връзка е изчерпала времето след определена продължителност, е полезна информация за доклада. Второ, enum-ът получава derive(Debug, Clone, PartialEq). PartialEq позволява да се сравняват състояния в тестове с assert_eq!, нещо, което ще използваш в урок 6.
/// Lo que se supo de un servicio después de consultarlo.
///
/// Cada variante lleva sus propios datos: así `Falla` no tiene código HTTP que
/// alguien pueda leer por error, y `Ok` no tiene mensaje de error.
#[derive(Debug, Clone, PartialEq)]
pub enum Estado {
/// Contestó con 2xx a tiempo.
Ok { codigo: u16, ms: u64 },
/// Contestó con 2xx, pero tardó más de [`UMBRAL_LENTO_MS`].
Lento { codigo: u16, ms: u64 },
/// No contestó, contestó con error, o se acabó el tiempo.
Falla { motivo: String, ms: u64 },
/// Nunca se llegó a consultar.
NoIntentado,
}
Enum-ът не замества всяко използване на булеви стойности, нито всяко използване на struct-ове. Булевата стойност си остава правилна за въпрос с два прости отговора, като „здрава ли е услугата?“. Struct си остава правилен за данни, които съществуват заедно едновременно, като име, URL и ограничение. Enum е подходящ, когато възможностите имат различни форми и програмата трябва да ги третира по различен начин.
match: решение за всяко състояние, без дупки
match сравнява стойност с шаблони и произвежда резултат. На фигурата всеки клон (arm) има формата шаблон => израз. Шаблонът идентифицира вариант и може да извлича данните му. В Estado::Ok { codigo, ms } имената във фигурните скоби създават локални променливи codigo и ms. В Estado::Falla(msg) msg получава низа, който носи вариантът.
Шаблонът .. означава „игнорирай останалите полета“. В клона на Lento програмата има нужда от продължителността, за да я отпечата, но не се нуждае от кода. Това е за предпочитане пред измислянето на име като _codigo, когато няма да го използваш: съобщава, че данната съществува и че това решение не зависи от нея. Ако не ти трябва нито едно поле на вариант с данни, можеш да напишеш Estado::Ok { .. }.
match е израз. Затова фигурата може да направи let texto = match estado { ... };. Всеки клон връща String: три използват format!, а последният строи един с "sin revisar".to_string(). Rust изисква всички разклонения да произвеждат съвместими типове. Това правило не позволява единият път да върне текст, а друг, случайно, да не върне нищо.
Най-ценното свойство е изчерпателността. Компилаторът познава всички варианти на Estado, защото са декларирани в същия тип. Ако един match не покрива някой от тях, не се компилира. Това е по-безопасно от switch, който позволява да се мине без действие, и също така е по-явно от верига от if, която оставя един случай като имплицитна възможност.
В някои случаи ще използваш заместващ шаблон, _ => ..., за да групираш възможности, които наистина трябва да получат еднакво третиране. Това е валидно, но има цена: ако добавиш нов вариант, този клон вече ще го приеме, без да те принуди да помислиш дали правилното поведение е същото. За централен enum като Estado е добре да предпочиташ явни клонове в доклада. Така един нов вариант се превръща във видимо решение, а не в случайно поведение.
Фиг. 3.3 | Ако забравиш един вариант, не се компилира.
// fig03_03.rs
enum Estado {
Ok { codigo: u16, ms: u64 },
Lento { codigo: u16, ms: u64 },
Falla(String),
NoIntentado,
}
fn main() {
let estado = Estado::NoIntentado;
let texto = match estado {
Estado::Ok { codigo, ms } => format!("OK {codigo} en {ms}ms"),
Estado::Lento { ms, .. } => format!("LENTO {ms}ms"),
Estado::Falla(msg) => format!("FALLA: {msg}"),
};
println!("{texto}");
}
$ rustc --edition 2024 fig03_03.rs
error[E0004]: non-exhaustive patterns: `Estado::NoIntentado` not covered
--> fig03_03.rs:11:23
|
11 | let texto = match estado {
| ^^^^^^ pattern `Estado::NoIntentado` not covered
|
note: `Estado` defined here
--> fig03_03.rs:2:6
|
2 | enum Estado {
| ^^^^^^
...
6 | NoIntentado,
| ----------- not covered
= note: the matched value is of type `Estado`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
14 ~ Estado::Falla(msg) => format!("FALLA: {msg}"),
15 ~ Estado::NoIntentado => todo!(),
|
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0004`.
В revisor match се появява в модула за доклади, за да превърне едно техническо състояние в етикет, който човек може да прочете. Забележи, че всеки вариант е назован изрично. Кодът не предполага, че „всичко, което не е OK, е повреда“: Lento и NoIntentado имат собствено значение.
fn etiqueta(e: &Estado) -> &'static str {
match e {
Estado::Ok { .. } => "OK",
Estado::Lento { .. } => "LENTO",
Estado::Falla { .. } => "FALLA",
Estado::NoIntentado => "NO",
}
}
Параметърът е &Estado, непроменлива референция. Докладът трябва да чете състоянието няколко пъти, за да получи етикет, продължителност и подробности, така че не е удачно да го консумира. Rust позволява да се прави match върху референция: шаблоните четат полетата, от които се нуждаят, без да преместват оригиналния Estado. Тази комбинация от заемания и шаблони ще е много честа в Rust код.
Съществува и matches!, полезен макрос, когато искаш само булев отговор. Методът esta_bien на проекта не трябва да произвежда текст, нито да извлича кодове; пита дали стойността е един от два здрави варианта. Групирането на шаблони с | изразява това правило, без да повтаря логика.
impl Estado {
/// `true` si el servicio contestó bien (aunque haya sido lento).
pub fn esta_bien(&self) -> bool {
matches!(self, Estado::Ok { .. } | Estado::Lento { .. })
}
}
Не използвай matches!, за да замениш match, който трябва да трансформира данни. Резултатът му винаги е bool; той е въпрос, не пълно решение. Когато програмата трябва да построи доклад, да вземе продължителност или да избере конкретна подробност, match си остава подходящият инструмент.
Option<T>: отсъствието, декларирано в типа
Rust няма null нито nil. Стойност от тип String винаги е валиден низ; стойност от тип &Servicio винаги е валидна референция, докато заемането е валидно. Когато една данна може да липсва, типът ѝ трябва да го декларира. Стандартният начин е Option<T>.
enum Option<T> {
Some(T),
None,
}
Реалната дефиниция принадлежи на стандартната библиотека и има повече вътрешни атрибути, но този фрагмент показва централната ѝ идея. Option<u16> означава „може да има код u16, а може и да няма“. Option<Servicio> означава „едно търсене може да върне услуга или да не намери никоя“. Типът не решава какво да се прави при отсъствие; задължава този, който консумира стойността, да го направи изрично.
Отсъствието не винаги е грешка. Търсенето на услуга по име може да не я намери, защото името не е конфигурирано. Един HTTP заглавен ред може да е по избор. Една мрежова повреда може да не произведе HTTP код. В тези случаи Option съобщава, че липсата на стойност е възможност, предвидена от договора, а не таен знак като 0, "" или нулев указател, който може да избухне по-късно.
Фиг. 3.4 | Консумиране на един Option.
// fig03_04.rs
fn main() {
let quizas: Option<u16> = Some(200);
match quizas {
Some(c) => println!("código {c}"),
None => println!("sin respuesta"),
}
if let Some(c) = quizas { println!("código {c}"); } // cuando solo importa un caso
let c = quizas.unwrap_or(0); // valor por omisión
println!("{c}");
}
$ rustc --edition 2024 fig03_04.rs && ./fig03_04
código 200
código 200
200
Първото консумиране използва match, защото двата случая имат значение: има изход за Some и друг за None. Второто използва if let, защото има работа за вършене само когато съществува код; ако не съществува, програмата не трябва да прави нищо. if let Some(c) = quizas е кратък начин да се напише match, чийто друг клон би бил _ => {}.
unwrap_or(0) връща съдържанието, когато съществува, и стойността по подразбиране, когато не. Решението да се използва 0 е правилно само ако извикващият разбира, че нулата представлява „няма отговор“ в този контекст. В публичен HTTP доклад може да е по-ясно да запазиш Option<u16> до мястото, където се представя данната, за да не объркаш едно отсъствие с реален HTTP код.
Не бъркай unwrap_or с unwrap. unwrap() казва: „знам, че тук има стойност; ако няма, прекрати програмата с panic!“. Може да е разумно в тест, където отсъствието показва, че подготовката на случая се е провалила, но в приложен код обикновено крие отложено решение. clippy с конфигурацията си по подразбиране не предупреждава за обикновен unwrap; съществува незадължително предупреждение (clippy::unwrap_used), което този, който поддържа проект, може да включи, за да го забрани. Урок 6 ще покаже как да изпълняваш clippy. Преди да го напишеш, попитай се дали None може да се случи в продукция. Ако може, трябва да го обработиш.
Проектът използва Option за JSON формата на доклада. Една повреда няма измислен HTTP код, затова codigo е Option<u16>. Полето error също е незадължително: появява се при повреда или при непопитана услуга, но се пропуска при здрав отговор. Този struct представя изход, който може да се сериализира, не замества вътрешния enum Estado; двата типа имат различни отговорности.
#[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>,
}
Това разделяне е полезно. Estado моделира изключващи се алтернативи, за да е безопасна вътрешната логика. EstadoJson моделира формата, който един външен инструмент очаква да прочете, където някои полета могат да са null или да се пропускат. Rust не пречи на един външен API да има незадължителни стойности; пречи на твоята вътрешна логика да ги третира, сякаш винаги съществуват.
Грешката, която ще видиш
E0004: един match не покрива всички шаблони
Фигура 3.3 произвежда E0004, „non-exhaustive patterns“. Компилаторът видя, че estado е от тип Estado, прочете дефиницията на enum-а и провери, че съществува вариантът NoIntentado. После обходи клоновете на match и не намери никакъв шаблон, който да го покрива.
Стрелката под estado показва стойността, върху която се взема решението. Следващата бележка показва къде е дефиниран Estado и подчертава липсващия вариант. Помощното съобщение предлага две поправки: да се добави изричен клон за Estado::NoIntentado или да се добави заместващ шаблон. В този случай правилната поправка е изричната, защото „sin revisar“ заслужава видим изход:
Estado::NoIntentado => "sin revisar".to_string(),
Не копирай todo!() от предложението като окончателно решение. Rust го предлага, защото допълва шаблона и оставя видим маркер, за да решиш какво да правиш; ако този клон се изпълни, todo!() прекратява програмата с panic!. Полезно е при кратко рефакториране, не като поведение на revisor.
Тази грешка се появява и когато добавиш нов вариант. Това е именно едно от предимствата ѝ. Вместо да разчиташ на ръчно търсене из хранилището, остави типа и компилатора да изброят местата, които трябва да решат как да отговорят на новото състояние. Поправи всяко с бизнес правило, не с _ => по рефлекс.
Четене на грешката като указание за промяна
Грешките на Rust обикновено имат четири части: стабилния код, като E0004, местоположението, бележки с контекст и помощно съобщение. Започни от кода и основното изречение; в този случай стигат, за да разбереш, че липсва вариант. После прочети бележката, за да потвърдиш типа, и помощното съобщение, за да научиш един синтактично валиден начин да го поправиш.
Помощта на компилатора не познава твоя домейн. Може да ти каже как да допълниш един match, но не може да реши дали едно ново състояние трябва да се брои за здраво, повредено, бавно или непроверено. Това решение остава твое. Предимството е, че Rust разделя двата проблема: гарантира ти, че не си забравил да третираш случая, и те оставя да определиш правилното третиране.
Можеш да поискаш разширено обяснение с rustc --explain E0004. Направи го, когато срещнеш код на грешка, който не разбираш. Не е нужно да запаметяваш кодове; важното е да се научиш да разпознаваш, че са идентификатори, които могат да се консултират, и че съобщението съдържа конкретно доказателство за типа и за реда, които са замесени.
Какво се прави погрешно
Моделиране на алтернативи със struct, пълен с незадължителни полета
Struct като Resultado { codigo: Option<u16>, error: Option<String>, ms: Option<u64> } изглежда гъвкав, но приема твърде много несвързани състояния. Може да съдържа едновременно успешен код и съобщение за повреда или да не съдържа нито едното, нито другото. Ако тези случаи са невалидни, типът трябва да ги прави трудни или невъзможни за построяване.
Използвай enum, когато възможностите се изключват взаимно и носят различни данни. Запази struct-ове с Option за външни граници, като JSON, формуляри или частични конфигурации, където наистина трябва да представиш полета, които могат да липсват независимо едно от друго.
Използване на _ => за заглушаване на изчерпателността
Заместващият шаблон е правилен, когато всички останали варианти получават точно същото третиране. Проблемът се появява, когато се използва само за да спре компилаторът да протестира. В бизнес enum _ може да превърне нов вариант в обща повреда или, още по-зле, в здрав отговор по погрешка.
В Estado напиши четирите клона изрично. Ако добавиш вариант, приеми, че компилаторът те принуждава да прегледаш доклада и тестовете. Малката работа сега избягва непрегледано поведение по-късно.
Писане на unwrap() в нормален път на програмата
unwrap() не решава отсъствието: превръща го в panic!. Ако една услуга може да не бъде намерена, един отговор може да не носи код или един файл може да не съществува, отсъствието е част от реалността на програмата. Трябва да се превърне в match, в if let, в оправдана стойност по подразбиране или, в урок 4, в Result.
В тестове unwrap() може да е полезен, за да декларираш, че един случай трябва да е подготвен правилно. В продукционна логика го използвай само когато си доказал, че None е невъзможно и провалът представлява програмна грешка, а не очаквано условие.
Копиране с clone(), за да не мислиш за ownership
Clone не е автоматичен изход при грешка за преместване. Копирането на Servicio само за да го заемеш на функция дублира низовете му String и може да скрие, че функцията трябва да получава &Servicio. На фигура 3.1 clone() съществува, за да демонстрира trait-а и да направи видима структурата; не е препоръчваният начин за предаване на услуги из програмата.
Предпочитай заемания за четене (&Servicio), променливи заемания, когато има реална промяна (&mut Servicio), и преместване, когато функцията трябва да вземе собствеността на стойността. Копирай само когато програмата наистина има нужда от две независими стойности.
Използване на магически числа или низове за представяне на състояния
Представянето на повреда с codigo == 0, на изчакваща заявка с ms == 0 или на грешка с mensaje == "" те принуждава да помниш конвенции, които типът не изразява. Затруднява и отговора на прости въпроси: може ли един реален отговор да отнеме нула милисекунди? празното съобщение повреда ли е, или отсъствие на повреда?
Именувай състоянието с вариант. Estado::NoIntentado съобщава повече от едно специално число и позволява match да те задължи да го обработиш. Когато състоянието има данни, постави ги вътре във варианта, който ги прави валидни.
Бъркане на Option с Result
Option<T> отговаря на „има ли стойност, или не?“. Result<T, E> отговаря на „имаше ли стойност, или имаше грешка, която трябва да знам?“. Едно търсене, което не намира име, може да върне Option<Servicio>; четенето на файл, който не съществува, обикновено трябва да върне Result<String, Error>, защото този, който извиква, трябва да знае какво се е объркало. Урок 4 разглежда Result и ? в дълбочина.
Не измисляй съобщения за грешка вътре в Option, нито използвай None, за да скриеш повреда, която потребителят трябва да диагностицира. Избери типа според договора на операцията.
Упражнения
Упражнение 1 — Опиши една услуга
Създай struct ServicioLocal с nombre: String, url: String и timeout_ms: u64. Напиши асоциирана функция new(nombre: &str, url: &str) -> Self, която задава 3000 като време по подразбиране. Добави метод etiqueta(&self) -> String, който връща nombre (url).
В main създай услуга, наречена pagos, отпечатай етикета и после отпечатай времевото ограничение. Провери, че за тези операции не ти трябват нито mut, нито clone().
Упражнение 2 — Обобщи всички състояния
Декларирай enum EstadoLocal с вариантите Ok { codigo: u16, ms: u64 }, Lento { codigo: u16, ms: u64 }, Falla(String) и NoIntentado. Напиши fn resumen(estado: &EstadoLocal) -> String с изчерпателен match.
Резюмето трябва да използва точно тези форми: OK 200 en 80ms, LENTO 1200ms, FALLA: sin conexión и sin revisar. Изпробвай го с по една инстанция от всеки вариант.
Упражнение 3 — Добави вариант и остави Rust да намери работата
Добави Rechazado { codigo: u16, ms: u64 } към EstadoLocal. Компилирай, без да променяш resumen, и наблюдавай E0004. После добави клона, който произвежда RECHAZADO 403 en 15ms.
Не използвай _ =>. Целта е да провериш, че компилаторът посочва едно чакащо бизнес решение. Обясни с едно изречение защо Rechazado не бива автоматично да се класифицира като Falla: сървърът наистина е отговорил, но отговорът не е бил приет.
Упражнение 4 — Търси, без да използваш nil
Създай масив или вектор от две ServicioLocal: catalogo и pagos. Напиши функция, която получава slice от услуги и име и връща Option<&ServicioLocal>. Търси първо pagos, а после reportes.
Консумирай първия резултат с if let, за да отпечаташ URL адреса му. Консумирай втория с match, за да отпечаташ no existe reportes. Не използвай индекси със сентинелна стойност, нулеви референции, нито unwrap().
Решения
Решение 1
ServicioLocal трябва да е struct с три именувани полета. Функцията new трябва да използва Self и да превръща nombre и url от &str в String; методът etiqueta трябва да получава &self, тъй като само чете полетата. Правилният изход съдържа:
pagos (http://localhost:8091/ok)
timeout: 3000 ms
Ако ти се налага да декларираш let mut servicio, прегледай упражнението: никоя от поисканите операции не променя стойността. Ако ти трябва clone(), вероятно си променил някоя сигнатура да получава self, когато е трябвало да получава &self.
Решение 2
resumen трябва да получава &EstadoLocal, за да чете състоянието, без да го консумира. Трябва да има четири явни клона. Клонът на Ok извлича codigo и ms; този на Lento може да използва ms и да игнорира кода с ..; този на Falla извлича съобщението; този на NoIntentado връща фиксирания текст.
Четирите извиквания трябва да произведат тези редове:
OK 200 en 80ms
LENTO 1200ms
FALLA: sin conexión
sin revisar
Ако един клон връща &str, а останалите връщат String, накарай всички да произвеждат един и същ тип. format! връща String; за фиксиран етикет можеш да използваш .to_string().
Решение 3
При добавянето на Rechazado компилацията трябва да се провали с E0004, докато не добавиш явен клон. Правилният клон извлича двете полета и произвежда:
RECHAZADO 403 en 15ms
Решението не се състои в смяната на последния клон с _ => "FALLA". Тази форма би накарала програмата да се компилира, но би загубила разликата между паднала мрежа и сървър, който е отговорил с политика за оторизация. Enum-ът предлага нова възможност; match трябва да я превърне в явно решение.
Решение 4
Функцията за търсене трябва да върне Option<&ServicioLocal>, не Option<ServicioLocal>. Референцията позволява да заемеш намерената услуга от списъка, без да копираш низовете ѝ и без да я местиш извън вектора. Едно търсене може да обходи услугите и да върне първата, чието nombre съвпада; ако не намери никоя, връща None.
За pagos if let Some(servicio) трябва да отпечата URL адреса ѝ. За reportes един match трябва да включва и двата клона и да произведе:
no existe reportes
Ако компилаторът се оплаче от lifetime-ове, прегледай сигнатурата: изходната референция трябва да произлиза от входния slice. В повечето от тези случаи Rust може да изведе правилния lifetime, без ти да го пишеш. Урок 5 ще обясни случаите, в които трябва да го декларираш.
Как да разбера, че съм успял
- Компилирам фигура 3.1 с
rustc --edition 2024 fig03_01.rs && ./fig03_01и получавам трите документирани реда. - Компилирам фигура 3.2 и мога да обясня защо всеки вариант на
Estadoноси различни данни. - Компилирам фигура 3.3, виждам
error[E0004]и я накарвам да се компилира, като добавя явен клон заNoIntentado. - Мога да добавя вариант към моя enum и да намеря всяко чакащо решение чрез грешките на
match. - Моето упражнение 4 връща
Option<&ServicioLocal>и обработва кактоSome, така иNoneбезunwrap(). - Мога да обясня защо
revisorизползва enum заEstadoи struct сOptionзаEstadoJson. - Завърших упражненията на Rustlings
structs,enumsиoptions.
За по-нататъшно четене
- The Rust Programming Language, глава 5: Using Structs to Structure Related Data — консултирано на 2 октомври 2026 г.
- The Rust Programming Language, глава 6: Enums and Pattern Matching — консултирано на 2 октомври 2026 г.
- Официална документация на
std::option::Option— консултирано на 2 октомври 2026 г. - Rustlings: упражнения за structs, enums и options — консултирано на 2 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx