Переводчик:NiZerin
Оригинальная ссылка:null ref.com/blog/rust-post…
Rust делает выражение условной компиляции очень простым, особенно благодаря своим «особенностям». Они хорошо интегрированы в язык и очень просты в использовании. Но одна вещь, которую я усвоил, поддерживая Rspotify, библиотеку для Spotify API, заключается в том, что люди не должны зацикливаться на них. Условную компиляцию следует использовать, когда условная компиляция является единственным способом решения проблемы по ряду причин, которые я объясню в этой статье.
Для кого-то это может быть очевидно, но не для меня, когда я начал использовать Rust. Это может быть забавным напоминанием, даже если вы уже это знаете; возможно, вы забыли об этом в своем последнем проекте и добавили ненужную функцию.
В условной компиляции нет ничего нового. С одной стороны, C и C++ делают это уже давно. То же самое можно применить и к этим ситуациям. Однако, по моему опыту, условную компиляцию гораздо проще использовать в Rust, а это означает, что ею также чаще злоупотребляют.
вопрос
Я столкнулся с этой дилеммой, когда решал, как настроить токены кеша в Rspotify. Библиотека позволяет постоянно управлять токенами аутентификации через файлы JSON. Таким образом, когда программа запускается снова, токен из предыдущего сеанса может быть повторно использован без повторного прохождения полной аутентификации, то есть до истечения срока действия токена.
Изначально это было что-то под названием cached_token, но я особо об этом не думал. Если вам это не нужно, зачем вам код для сохранения и чтения файла токена? Самый простой способ — использовать функцию, которую вы можете добавить в свой Cargo.toml.
Однако позже мне понадобилась еще одна очень похожая функция, refreshing_token.При необязательном включении клиент будет автоматически обновлять токены с истекшим сроком действия. Поскольку этот шаблон появляется все чаще и чаще в библиотеке, я хочу убедиться, что он спроектирован так, чтобы быть оптимальным. Покопавшись глубже, я начал обнаруживать ряд неудобств с функцией:
Они негибкие: в одной программе нельзя иметь клиента с кешированными токенами и другого без них. Это относится ко всей библиотеке, поэтому вы либо включаете их, либо нет. Очевидно, что они также не могут быть настроены во время выполнения; пользователь может захотеть выбрать, какому поведению следовать во время работы программы.
Они уродливы: письмо#[cfg(feature = "cached_token")]
Странный и более многословный, чем обычноif cached_token
.
Они беспорядочны: функциями в кодовой базе трудно управлять. Вы можете легко найти себя в Rust с эквивалентом#ifdef
ад.
Их сложно задокументировать и протестировать: Rust не предоставляет возможности раскрыть функциональность библиотеки. Все, что вы можете сделать, это перечислить их вручную на главной странице документа. Тестирование также сложнее, потому что вам нужно выяснить, какие комбинации функций использовать для охвата всей кодовой базы и применять их, когда вы хотите запускать тесты.
Все это считается важным только для того, чтобы гарантировать, что двоичный файл не будет содержать код, который вам не нужен. Но насколько это реально, на самом деле? Насколько это важно?
альтернатива
Оказывается, одна из самых простых оптимизаций, которую может реализовать компилятор, — это распространение констант. Это, в сочетании с удалением мертвого кода, может иметь тот же эффект, что и трейт, но более естественным образом. Вы можете сделать то же самое со структурой Config, за исключением добавления функциональности для настройки поведения программы. Вам может даже не понадобиться структура, если это просто возможность настройки, но тогда она рассчитана на будущее. Например:
#[derive(Default)]
struct Config {
cached_token: bool,
refreshing_token: bool,
}
Затем вы можете изменить свой клиент, чтобы опционально принять структуру Config:
struct Client {
config: Config
}
impl Client {
/// Uses the default configuration for the initialization
fn new() -> Client {
Client {
config: Config::default(),
}
}
/// Uses a custom configuration for the initialization
fn with_config(config: Config) -> Client {
Client {
config,
}
}
fn do_request(&self) {
if self.config.cached_token {
println!("Saving cache token to the file!");
}
// The previous block used to be equivalent to:
//
// #[cfg(feature = "cached_token")]
// {
// println!("Saving cache token to the file!");
// }
if self.config.refreshing_token {
println!("Refreshing token!");
}
println!("Performing request!");
}
}
Наконец, пользователи могут настроить клиент, которого они хотят, в коде очень естественным образом:
fn main() {
// Option A
let client = Client::new();
// Option B
let config = Config {
cached_token: true,
..Default::default()
};
let client = Client::with_config(config);
}
Докажите, что вы в конечном итоге с тем же кодом
Благодаря превосходному Compiler Explorer мы можем использовать следующий фрагмент, чтобы убедиться, что компиляция идет так, как мы ожидали:
Кажется, начиная с Rust 1.53, дляopt-level
При значениях больше или равных 2 код устаревшей функции даже не отображается в сборке (это легко увидеть, взглянув на строку в конце).cargo build --release
настроитьopt-level
3, так что это не должно быть проблемой для производственных двоичных файлов.
мы даже не использовалиconst
! Я хотел бы знать, что происходит в этом случае. Используйте этот слегка измененный фрагмент:
Что ж. На самом деле мы получили тот же результат. Полученная сборка точно такая же, только необязательный код изменен сopt-level=2
.
проблема в этомconst
Просто означает, что его значение может (но не должно) быть встроено. нет других. Так что у нас по-прежнему нет никаких гарантий, что встраивания недостаточно для упрощения кода внутри функции.
Поэтому для того, что я исследую, лучше не беспокоиться об этом и вместо этого использовать переменные.const
, это выглядит лучше и дает тот же результат.
в любом случае
Действительно ли необязательный код вредит конечному двоичному файлу, даже если предыдущая оптимизация не реализована? Не переусердствуем ли мы с нашими решениями, как всегда? Дело в том, что необязательный код для кэширования/обновления токенов даже не настолько раздут.
Конечно, это зависит от обстоятельств, но, на мой взгляд, раздувание бинарных файлов не является большой проблемой для двоичных файлов более высокого уровня. Rust уже статически встраивает свою стандартную библиотеку, среду выполнения и большое количество отладочной информации в каждый двоичный файл общим размером около 3 МБ. Единственные накладные расходы, которые могут возникнуть во время выполнения, — это ветвление.
В заключение
Иногда вам просто нужно использовать условную компиляцию, без нее не обойтись. Возможно, вы работаете над кодом для конкретной платформы или хотите уменьшить количество зависимостей для ящика, и в этом случае функции полезны.
Но это не относится к Rspotify. Условная компиляция определенно не подходит. Когда вы собираетесь добавить новую функцию в свой ящик, подумайте про себя: «Действительно ли мне нужна условная компиляция?».
Ни в соответствии с обычными рассуждениями, ни cached_token, ни refreshing_token не соответствуют причине, по которой может быть добавлена функциональность. Они не разрешают доступ к новым функциям/модулям. Они не помогают избавиться от необязательных зависимостей. И они, конечно же, не являются специфическими для платформы функциями. Они просто настраивают поведение библиотеки.
Чтобы этого не произошло, может быть, названия функций можно было бы изменить? Включение поддержки кэшированных токенов звучит как «функция», а код, специфичный для ОС, не кажется реальной функцией. Я также иногда нахожу это запутанным, и Google соглашается со мной в этом вопросе. Поиск информации, связанной с функциями Rust, может дать что-то совершенно не относящееся к делу, так как в результате есть слово «функция», но оно означает «атрибут или аспект программы». Вроде как вам нужно гуглить «golang X» вместо «go X», иначе это не имеет смысла. Но в любом случае, мое мнение запоздало.