как компилятор доказывает отсутствие висячих ссылокhow the compiler proves the absence of dangling references
Lifetime — имя ('a, 'b, ...), данное области кода, для которой ссылка остаётся валидной. Аннотации времени жизни не продлевают жизнь значения — они описывают уже существующее отношение между временами жизни разных ссылок, чтобы borrow checker мог его проверить.A lifetime is a name ('a, 'b, ...) given to the region of code for which a reference stays valid. Lifetime annotations don't make a value live longer — they describe an already-existing relationship between different references' lifetimes so the borrow checker can verify it.
Lifetime — это имя, которое даётся области кода (scope), на протяжении которой ссылка остаётся валидной; используется в сигнатурах функций и в определениях структур, чтобы сообщить компилятору, как времена жизни разных ссылок связаны друг с другом. Важно: lifetime НЕ заставляет ссылку жить дольше — это распространённое заблуждение. Он лишь описывает уже существующее отношение, чтобы borrow checker мог его проверить. Например, fn longest<'a>(x: &'a str, y: &'a str) -> &'a str говорит: возвращаемая ссылка валидна ровно столько, сколько валидны ОБА параметра x и y — на практике это пересечение их времён жизни, обычно равное более короткому из двух.A lifetime is a name given to the scope for which a reference is valid; it's used in function signatures and struct definitions to tell the compiler how different references' lifetimes relate to each other. Importantly: a lifetime does NOT make a reference live longer — that's a common misconception. It only describes an already-existing relationship so the borrow checker can verify it. For example, fn longest<'a>(x: &'a str, y: &'a str) -> &'a str says: the returned reference is valid for exactly as long as BOTH x and y are valid — in practice, their intersection, usually equal to the shorter of the two.
Без явных аннотаций времени жизни компилятор не мог бы проверить целый класс кода:Without explicit lifetime annotations, the compiler couldn't verify a whole class of code:
Функция, принимающая несколько ссылочных параметров и возвращающая ссылку, оставляет открытым вопрос: от какого именно входного параметра зависит время жизни результата? Или это смесь обоих? Или результат вообще не связан ни с одним из входов? Без аннотации у компилятора нет способа это узнать — а без этого знания он не может доказать отсутствие висячей ссылки на выходе.A function taking multiple reference parameters and returning a reference leaves open the question: which input parameter does the result's lifetime actually depend on? Or is it a mix of both? Or is the result unrelated to either input? Without an annotation, the compiler has no way to know — and without that knowledge, it cannot prove the absence of a dangling reference on the output.
Программист обычно и так держит это отношение в голове ("результат живёт не дольше более короткого из двух входов"). Lifetime-аннотации делают это отношение явным и машинно проверяемым, вместо того чтобы оставлять его неявным допущением, которое компилятор не может подтвердить.The programmer usually already has this relationship in mind ("the result doesn't outlive the shorter of the two inputs"). Lifetime annotations make that relationship explicit and machine-checkable, instead of leaving it as an implicit assumption the compiler cannot confirm.
Без этой проверки баг с висячей ссылкой либо проявился бы как classic C++-style undefined behavior в рантайме, либо — что хуже — не проявился бы вообще до конкретного неудачного расположения памяти, которое триггерит крэш через месяцы после релиза. Lifetimes переносят эту проверку целиком на этап компиляции.Without this check, a dangling-reference bug would either surface as classic C++-style undefined behavior at runtime, or — worse — not surface at all until one particular unlucky memory layout triggers a crash months after release. Lifetimes move this check entirely to compile time.
Классический пример — функция, возвращающая более длинную из двух строк:The classic example — a function returning the longer of two strings:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// без 'a компилятор откажется собирать: "missing lifetime specifier"// without 'a the compiler refuses to build: "missing lifetime specifier"
Большинству функций явные аннотации не нужны благодаря трём "правилам элизии времён жизни" (lifetime elision rules), которые компилятор применяет автоматически: (1) каждый элидированный входной ссылочный параметр получает собственный lifetime; (2) если входной lifetime ровно один, он присваивается всем элидированным выходным ссылкам; (3) если один из параметров — &self/&mut self, время жизни self присваивается всем элидированным выходным ссылкам.Most functions need no explicit annotations at all, thanks to three "lifetime elision rules" the compiler applies automatically: (1) each elided input reference gets its own lifetime parameter; (2) if there's exactly one input lifetime, it's assigned to all elided output lifetimes; (3) if one of the parameters is &self/&mut self, self's lifetime is assigned to all elided output lifetimes.
Структура, хранящая ссылку, сама должна нести параметр времени жизни — иначе компилятор не смог бы гарантировать, что данные, на которые она ссылается, не будут уничтожены раньше самой структуры:A struct holding a reference must itself carry a lifetime parameter — otherwise the compiler couldn't guarantee the data it references won't be dropped before the struct itself:
struct Excerpt<'a> {
part: &'a str, // Excerpt не может пережить строку, на которую ссылается part// Excerpt cannot outlive the string that part refers to
}
Без аннотации компилятор не знает, зависит ли результат от x, от y или от обоих — сборка падает с ошибкой "missing lifetime specifier". С 'a, связывающим оба параметра и результат, компилятор может проверить, что вызывающий код не использует результат дольше, чем валидны оба входа.Without the annotation, the compiler doesn't know whether the result depends on x, on y, or on both — the build fails with "missing lifetime specifier". With 'a tying both parameters and the result together, the compiler can verify the caller doesn't use the result longer than both inputs remain valid.
// fn longest(x: &str, y: &str) -> &str { ... } // ОШИБКА: missing lifetime specifier// fn longest(x: &str, y: &str) -> &str { ... } // ERROR: missing lifetime specifier
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { // OK// OK
if x.len() > y.len() { x } else { y }
}
Когда результат производится только от x, а y используется, но не возвращается, аннотация может быть точнее: время жизни y вообще не связывается с результатом.When the result is derived only from x, and y is used but not returned, the annotation can be more precise: y's lifetime isn't tied to the result at all.
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
x // только x связан с 'a результата, _y — независимый, отдельный lifetime// only x is tied to the result's 'a, _y has its own independent lifetime
}
Структура хранит &str в исходные данные, а метод, возвращающий ссылку, не нуждается в явной аннотации — правило элизии №3 (self) автоматически присваивает время жизни self выходной ссылке.The struct holds a &str into the source data, and a method returning a reference needs no explicit annotation — elision rule #3 (self) automatically assigns self's lifetime to the output reference.
struct Parser<'a> { input: &'a str, pos: usize }
impl<'a> Parser<'a> {
fn remaining(&self) -> &str { // элидировано: то же 'a, что и у &self// elided: the same 'a as &self
&self.input[self.pos..]
}
}
Строковые литералы имеют тип &'static str, потому что они встроены прямо в бинарник и живут всю программу. 'static уместен для данных, реально встроенных в бинарник или по-настоящему глобальных/утечных. Частый антипаттерн новичков — приписать 'static к границе-параметру просто чтобы заставить ошибку borrow checker'а исчезнуть; обычно это не решает проблему, а лишь переносит её либо вынуждает к ненужному Box::leak/клонированию.String literals have type &'static str because they're embedded directly in the binary and live for the whole program. 'static is appropriate for data genuinely embedded in the binary or truly global/leaked. A common newcomer anti-pattern is slapping 'static on a bound just to make a borrow-checker error go away; this usually doesn't solve the problem, only moves it, or forces an unnecessary Box::leak/clone.
let s: &'static str = "hello"; // литерал — валиден всю программу// literal — valid for the whole program
// fn bad<T: 'static>(x: T) { ... } // антипаттерн: 'static "чтобы ошибка исчезла"// fn bad<T: 'static>(x: T) { ... } // anti-pattern: 'static "to make the error go away"
Lifetime-аннотации ничего не меняют в том, сколько реально живёт значение, — они лишь описывают ограничение, которое borrow checker проверяет относительно уже определённых, реальных областей видимости в коде. Написать 'a где-то в сигнатуре не заставит значение жить дольше ни на одну инструкцию.Lifetime annotations don't change how long any value actually lives — they only describe a constraint the borrow checker verifies against the real, already-determined scopes in the code. Writing 'a somewhere in a signature doesn't make a value live a single instruction longer.
struct Excerpt<'a> ограничена временем жизни ссылки, которую хранит, — каждый экземпляр такой структуры сам не может пережить исходные данные. Это часто удивляет тех, кто пытается сохранить такую структуру там, где она должна жить дольше, чем исходные данные. Исправление обычно в том, чтобы структура ВЛАДЕЛА данными (например, String вместо &str), а не в борьбе с lifetime.struct Excerpt<'a> is bounded by the lifetime of the reference it holds — every instance of such a struct cannot itself outlive the source data. This often surprises people trying to store such a struct somewhere that needs to live longer than the original data. The fix is usually to have the struct OWN the data (e.g. String instead of &str), not to fight the lifetime.
Как только сигнатура функции не подпадает ни под одно из трёх правил элизии (например, два входных ссылочных параметра без self и возвращаемая ссылка), компилятор требует явную аннотацию и не пытается угадать — даже если человеку "правильное" отношение очевидно из контекста.As soon as a function's signature doesn't match one of the three elision patterns (e.g. two input references with no self and a returned reference), the compiler requires an explicit annotation and won't guess — even if a human could obviously figure out the "right" relationship from context.