История вопроса
Система типов Rust делит параметры продолжительности на "с ранним связыванием" и "с поздним связыванием". Параметры продолжительности с ранним связыванием разрешаются на момент определения или инстанцирования, становясь конкретными и фиксированными на протяжении существования элемента. Параметры продолжительности с поздним связыванием, вводимые с помощью синтаксиса for<'a> в HRTB, остаются полиморфными до фактической точки использования, позволяя функции или границе признаков работать унифицированно с любой возможной продолжительностью. Это различие возникло из-за необходимости поддерживать истинные функции высшего порядка — те, которые принимают обратные вызовы или замыкания, которые сами манипулируют заимствованными данными, не заставляя вызывающего вскоре отдавать предпочтение одной конкретной продолжительности для всех вызовов.
Проблема
Когда функция высшего порядка объявляет явный параметр продолжительности в своей сигнатуре, например, fn process<'a, F: Fn(&'a Data)>(f: F), продолжительность 'a становится ранним связыванием. Это означает, что компилятор выбирает конкретную продолжительность 'a на месте вызова на основе контекста, и тип замыкания F должен удовлетворять Fn(&'a Data) для этой конкретной 'a только. Следовательно, замыкание не может быть повторно использовано с данными различных продолжительностей в последующих вызовах, и попытка передать его в контекст, в котором продолжительность заимствования короче или длиннее, вызывает ошибку несовпадения продолжительностей. Это ограничение фактически препятствует созданию гибких, многоразовых абстракций, таких как пулы потоков или диспетчеры событий, которые должны обрабатывать временные заимствования.
Решение
HRTB решает эту проблему, перемещая параметр продолжительности в саму границу признаков: fn process<F: for<'a> Fn(&'a Data)>(f: F). Здесь for<'a> утверждает, что тип F реализует границу для каждой возможной продолжительности 'a, а не только для одной. Это делает продолжительность с поздним связыванием; компилятор проверяет, что замыкание универсально полиморфно, позволяя ему принимать ссылки с любой продолжительностью в каждом отдельном месте вызова внутри тела функции. Этот механизм отделяет хранение обратного вызова от продолжительности данных, позволяя абстракциям без затрат, которые безопасно обрабатывают заимствованные данные в различных контекстах выполнения.
// С ранним связыванием: 'a фиксирован на месте вызова, ограничивая гибкость fn bad_process<'a, F>(f: F) где F: Fn(&'a str) -> usize, { let local = String::from("temp"); // ОШИБКА: local не живет столько же, сколько раннее связывание 'a // f(&local); } // С поздним связыванием: HRTB позволяет 'a быть любой продолжительностью при каждом вызове fn good_process<F>(f: F) где F: for<'a> Fn(&'a str) -> usize, { let local = String::from("temp"); // ОК: 'a экземпляризуется как продолжительность &local только для этого вызова println!("{}", f(&local)); } fn main() { let count_fn = |s: &str| s.len(); good_process(count_fn); }
Описание проблемы
При проектировании системы диспетчеризации событий без копирования для высокочастотного торгового движка команде нужна была регистрация обработчиков стратегий. Эти обработчики были замыканиями, которые проверяли пакеты рыночных данных, не забирая их, что позволяло обрабатывать данные за микросекунды. Центральный диспетчер должен был хранить эти обработчики в HashMap<String, Box<dyn Handler>> и вызывать их с временными представлениями входящих сетевых буферов. Проблема заключалась в том, что сетевые буферы имели крайне короткие, ограниченные продолжительностью жизни, в то время как сам диспетчер был долгоживущим синглтоном. Если бы граница признаков обработчика была связана с конкретной продолжительностью, диспетчеру требовался бы этот параметр продолжительности, что сделало бы невозможным хранение в глобальном состоянии или выживание через разные торговые сессии.
Решение A: Статическая диспетчеризация с параметризацией продолжительности
Одним из подходов было сделать диспетчер универсальным по отношению к 'a, сохраняя Box<dyn Handler<'a>>. Это потребовало бы, чтобы вся структура диспетчера несла продолжительность 'a, эффективно делая его краткоживущим объектом, связанным со сроком действия сетевого буфера. Плюсы включали абстракции без затрат и отсутствие накладных расходов времени выполнения. Однако минусы были архитектурными: диспетчер не мог быть сохранен в lazy_static! или отправлен в другие потоки с независимыми продолжительностями, что вынуждало полностью переработать логику управления сессиями.
Решение B: Стертые продолжительности через границы 'static
Другой вариант заключался в том, чтобы требовать, чтобы все данные, передаваемые в обработчики, были 'static или заставлять обработчики брать собственные данные (например, Vec<u8>). Это позволяло хранить обработчики как Box<dyn Handler + 'static>. Плюсы заключались в простоте и легкости хранения. Минусы включали серьезные штрафы по производительности: каждый сетевой пакет требовал бы выделения и копирования для продвижения к 'static или собственному статусу, разрушая требования к задержке в микросекундах и увеличивая нагрузку на память во время высокой пропускной способности.
Решение C: Границы признаков высшего порядка (HRTB)
Выбранное решение определило границу признаков с использованием HRTB: trait Handler { fn handle(&self, data: &Packet); }, реализованную для F: for<'a> Fn(&'a Packet). Это позволяло хранить Box<dyn Handler> (по умолчанию 'static, потому что он обещает работать для любой продолжительности) при этом передавая эпhemerные заимствования сетевых буферов во время вызова handle. Плюсы заключались в сохранении производительности без копирования и способности хранить обработчики в долгоживущем, глобальном состоянии. Минусы включали повышенную сложность в границах признаков и необходимость следить за тем, чтобы обработчики случайно не захватывали ссылки из своей среды, которые бы нарушали контракт for<'a>.
Результат
Торговый движок успешно обрабатывал миллионы событий в секунду без выделения для данных пакетов. Архитектура на основе HRTB позволила команде смешивать и сочетать обработчики из различных модулей — некоторые заимствовали из стека, другие из локальных арены потоков — в то время как компилятор гарантировал, что ни один обработчик не мог пережить временные данные, к которым он обращался, предотвращая гонки данных и использование после освобождения в высококонкуррентной среде.
Почему Box<dyn Fn(&'a T)> заставляет параметр продолжительности вводиться в содержащую структуру, в то время как Box<dyn for<'a> Fn(&'a T)> не делает этого?
В первом случае продолжительность 'a является конкретным параметром типа самого объектного признака. Тип dyn Fn(&'a T) неявно несет привязку 'a, что означает, что объект признака действителен только для этой конкретной продолжительности. Следовательно, любая структура, содержащая его, должна объявлять <'a>, чтобы доказать, что структура не переживает ссылки, которые замыкание может захватить или принять. С for<'a> объект признака утверждает, что замыкание работает для всех продолжительностей, эффективно стирая конкретную зависимость от 'a из сигнатуры типа контейнера. Это позволяет структуре быть 'static, так как она держит обещание универсальной применимости, а не связывания с конкретным заимствованием.
Как HRTB взаимодействуют с замыканиями, которые пытаются вернуть ссылки на заимствованное входное значение?
Кандидаты часто пытаются написать F: for<'a> Fn(&'a T) -> &'a U, ожидая, что продолжительность вывода будет совпадать с входом. Однако связанный тип Output стандартного признака Fn не является универсальным по отношению к 'a; он фиксирован для типа замыкания. Следовательно, HRTB один только не может выразить тип возврата, продолжительность которого была бы связана с входным аргументом внутри семейства признаков Fn. Чтобы достичь этого, необходимо использовать обобщенные связанные типы (GATs) в сочетании с HRTB, определив пользовательскую границу признаков, такую как trait Processor { type Output<'a>; fn process<'a>(&self, input: &'a T) -> Self::Output<'a>; }. Не понимая этого ограничения, кандидаты часто сталкиваются с ошибками компилятора, указывающими на то, что тип возврата "не живет достаточно долго", ошибочно полагая, что HRTB может решить проблему продолжительности возврата в стандартных замыканиях.
Каково основное различие между продолжительностью с ранним связыванием в функции и продолжительностью с поздним связыванием в границе признаков в отношении монопринципа?
Когда функция объявляет свою собственную продолжительность, как в fn foo<'a, F: Fn(&'a T)>, продолжительность 'a является ранним связыванием. Во время монопринципа или проверки типов на месте вызова компилятор выбирает одну конкретную 'a, która удовлетворяет всем ограничениям для данного конкретного вызова. Тип F затем проверяется на соответствие этой конкретной 'a. В отличие от этого, с fn foo<F: for<'a> Fn(&'a T)> компилятор проверяет, что F удовлетворяет границе для всех возможных продолжительностей универсально. Это означает, что внутри foo вы можете вызывать замыкание несколько раз с аргументами разных продолжительностей, в то время как в случае раннего связывания все вызовы внутри foo будут ограничены единственной 'a, выбранной при вызове foo. Кандидаты часто упускают, что продолжительности с ранним связыванием в функциях действуют как "константы времени компиляции" для этого вызова, в то время как продолжительности с поздним связыванием в HRTB действуют как "универсально квантифицированные переменные", действительные для любой инстанциации.