% Проверка удаления

Мы уже посмотрели, как времена жизни предоставляют нам простые правила, гарантирующие, что мы никогда не прочитаем висячий указатель. В то же время до этого мы имели дело только с отношениями вида, 'a переживает 'b, которые сформулированы включительно. То есть, когда мы говорим о 'a: 'b, то в порядке вещей для 'a жить ровно столько же сколько 'b. На первый взгляд это кажется бессмысленным. Ничего же не может удалится одновременно с другим, правда? Вот почему утверждение let без синтаксического сахара выглядит следующим образом:

let x;
let y;
{
    let x;
    {
        let y;
    }
}

Каждое let создает свою область видимости, ясно утверждая, что одно удаляется после другого. А что, если мы сделаем так?

let (x, y) = (vec![], vec![]);

Живет ли одно значение дольше другого? Ответ, нет, ни одно значение не живет дольше другого. Конечно, или x, или y удалится одно после другого, но сам порядок не определен. Кортежи не одиноки в этом вопросе; начиная с Rust 1.0, составные структуры тоже не гарантируют порядок своего удаления.

Мы могли бы указать порядок для внутренних полей составных объектов, таких как кортежи или структуры. Но как насчет Vec или чего-то подобного? Vec приходится вручную удалять элементы посредством вызова кода из библиотеки. В общем случае все, что реализует Drop, имеет возможность повозиться со своими внутренностями, когда уже звучит похоронный звон. Поэтому компилятор не может достаточно точно определить порядок удаления полей типа, реализующего Drop.

Ну, а нам то какая разница? На самом деле это важно, потому что если система типов будет неаккуратна, она может случайно создать висячий указатель. Предположим, что у нас есть следующая программа:

struct Inspector<'a>(&'a u8);

fn main() {
    let (inspector, days);
    days = Box::new(1);
    inspector = Inspector(&days);
}

Программа абсолютно правильна и успешно компилируется. Тот факт, что days не живет строго дольше inspector не важен. Пока жив inspector, живы и days.

Но если мы добавим деструктор, программа больше не будет компилироваться!

struct Inspector<'a>(&'a u8);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Я был всего в {} днях от пенсии!", self.0);
    }
}

fn main() {
    let (inspector, days);
    days = Box::new(1);
    inspector = Inspector(&days);
    // Предположим, что `days` удалилась первой.
    // Когда Inspector будет удаляться, он попытается прочитать освобожденную 
    // память!
}
<anon>:12:28: 12:32 error: `days` does not live long enough
<anon>:12     inspector = Inspector(&days);
                                     ^~~~
<anon>:9:11: 15:2 note: reference must be valid for the block at 9:10...
<anon>:9 fn main() {
<anon>:10     let (inspector, days);
<anon>:11     days = Box::new(1);
<anon>:12     inspector = Inspector(&days);
<anon>:13     // Предположим, что `days` удалилась первой.
<anon>:14     // Когда Inspector будет удаляться, он попытается прочитать освобожденную память!
          ...
<anon>:10:27: 15:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 10:26
<anon>:10     let (inspector, days);
<anon>:11     days = Box::new(1);
<anon>:12     inspector = Inspector(&days);
<anon>:13     // Предположим, что `days` удалилась первой.
<anon>:14     // Когда Inspector будет удаляться, он попытается прочитать освобожденную память!
<anon>:15 }

Добавление Drop позволяет Inspector выполнить на смертном одре произвольный код. Это означает, что он, теоретически, может заметить, что типы, которые должны были бы жить столько же, сколько и он, на самом деле уже уничтожены.

Самое интересное, что только обобщённые типы должны об этом волноваться. Для не обобщённых типов время жизни может быть только 'static, а значит они могут жить вечно. Именно поэтому проблема называется проблемой правильного удаления обобщённых типов. Правильное удаление обобщённых типов гарантируется анализатором удалений. Во время написания этой главы некоторые детали, связанные с тем, как анализатор удалений проверяет типы, еще находились в подвешенном состоянии. Однако, Главным Правилом, на котором мы фокусируемся в этом разделе, будет следующее:

Чтобы обобщённый тип правильно реализовывал Drop, его обобщённые аргументы должно жить строго дольше него самого

Для того, чтобы подчиниться этому правилу, (обычно) необходимо удовлетворить требования анализатора заимствований; достаточно подчинятся этому правилу, хотя это и не обязательно. Таким образом, если ваш тип подчиняется этому правилу, то он точно правильно удалится.

Причиной, по которой не всегда обязательно подчиняться правилу выше, является то, что некоторые реализации Drop не обладают доступом к заимствованным данным, даже при том, что их тип обладает возможностью такого доступа.

Например, у этого варианта Inspector никогда не будет доступа к заимствованным данным:

struct Inspector<'a>(&'a u8, &'static str);

impl<'a> Drop for Inspector<'a> {
    fn drop(&mut self) {
        println!("Inspector(_, {}) знает, когда *не* проверять.", self.1);
    }
}

fn main() {
    let (inspector, days);
    days = Box::new(1);
    inspector = Inspector(&days, "gadget");
    // Предположим, что `days` удалилась первой.
    // Даже когда Inspector будет удаляться, у его деструктора не будет доступа
    // к заимствованным `days`.
}

У следующего варианта Inspector тоже никогда не будет доступа к заимствованным данным:

use std::fmt;

struct Inspector<T: fmt::Display>(T, &'static str);

impl<T: fmt::Display> Drop for Inspector<T> {
    fn drop(&mut self) {
        println!("Inspector(_, {}) знает, когда *не* проверять.", self.1);
    }
}

fn main() {
    let (inspector, days): (Inspector<&u8>, Box<u8>);
    days = Box::new(1);
    inspector = Inspector(&days, "gadget");
    // Предположим, что `days` удалилась первой.
    // Даже когда Inspector будет удаляться, у его деструктора не будет доступа
    // к заимствованным `days`.
}

Однако оба этих варианта будут отвергнуты анализатором заимствований во время проверки fn main, говоря, что days не живет достаточно долго.

Анализатор заимствований не знает о внутренностях каждой реализации Drop для Inspector при проверке main. Проверяя main, он понимает, что тело деструктора Inspector может получить доступ к заимствованным данным.

По этой причине анализатор удалений требует, чтобы все заимствованные данные в значении типа жили строго дольше значения этого типа.

Аварийный люк

Четкие правила, управляющие проверкой удалений, возможно, в будущем будут менее строгими.

Текущая проверка нарочито консервативна и тривиальна; она требует, чтобы все заимствованные данные в значении типа жили строго дольше значения этого типа, что несомненно правильно.

Будущие версии языка, возможно, сделают проверки более точными, чтобы уменьшить количество случаев, когда правильный код отвергается как небезопасный. Это помогло бы решить случаи, как с двумя Inspector выше, которые знают, что не надо проверять их во время удаления.

В настоящее время существует нестабильный атрибут, который можно использовать, чтобы указать (небезопасно), что деструктор обобщенного типа гарантированно не будет иметь доступ к просроченным данным, даже при том, что у такого типа есть возможность сделать это.

Аттрибут называется unsafe_destructor_blind_to_params. Чтобы применить его к Inspector из примера выше, напишем:

struct Inspector<'a>(&'a u8, &'static str);

impl<'a> Drop for Inspector<'a> {
    #[unsafe_destructor_blind_to_params]
    fn drop(&mut self) {
        println!("Inspector(_, {})  знает, когда *не* стоит проверять.", self.1);
    }
}

Внутри аттрибута есть слово unsafe, потому что компилятор не проверяет, что осуществляется доступ к потенциально просроченным данным (здесь, например, к self.0).

Иногда очевидно, что такой доступ не происходит, как в примере выше. Однако, когда мы имеем дело с параметром обобщенного типа, такой доступ может произойти не напрямую. Примерами такого косвенного доступа являются:

  • выполнение обратного вызова,
  • вызов метода типажа.

(Будущие изменения в языке, такие как специализация impl, могут добавить другие возможности такого косвенного доступа.)

Вот пример с выполнением обратного вызова:

struct Inspector<T>(T, &'static str, Box<for <'r> fn(&'r T) -> String>);

impl<T> Drop for Inspector<T> {
    fn drop(&mut self) {
        // Вызов `self.2` может получить доступ к заимствованию, например, если `T` это `&'a _`.
        println!("Inspector({}, {}) нечаянно проверяет просроченные данные.",
                 (self.2)(&self.0), self.1);
    }
}

Вот пример с вызовом метода типажа:

use std::fmt;

struct Inspector<T: fmt::Display>(T, &'static str);

impl<T: fmt::Display> Drop for Inspector<T> {
    fn drop(&mut self) {
        // Ниже есть невидимый вызов `<T as Display>::fmt`, который 
        // может получить доступ к заимствованию, например, если `T` это `&'a _`
        println!("Inspector({}, {}) нечаянно проверяет просроченные данные.",
                 self.0, self.1);
    }
}

И конечно, все эти получения доступа могут быть дополнительно скрыты внутри других методов, вызываемых деструктором, а не написанных непосредственно внутри него.

Во всех случаях выше, где доступ к &'a u8 получается в деструкторе благодаря добавлению аттрибута #[unsafe_destructor_blind_to_params], становится возможным неправильно использовать тип, это не поймает анализатор заимствований, и возникнет хаос. Лучше избежать добавление этого аттрибута.

Это все о проверке удалений?

Мы выяснили, что при написании небезопасного кода, нам обычно не надо волноваться о том, как правильно пройти проверки удалений. Но есть один особый случай, в котором надо волноваться об этом. Рассмотрим его в следующем разделе.