Когда-нибудь выходили из себя, набирая тот же старый код шаблона снова и снова? Заметили закономерность и бум! Rust может взорвать код для тебя!
Как разработчик программного обеспечения, я пишу заметное количество кода. Приятно печатать и видеть поток характеристик, проявляющих мои мысли. Однако мой мыслительный процесс имеет тенденцию двигаться быстрее, чем мои пальцы на клавиатуре. Чтобы устранить это несоответствие, я иногда ищу способы минимизировать ввод текста, сохраняя скорость мышления.
В эти моменты я могу распознать возможности оптимизации, такие как извлечение повторяющегося кода в выделенную функцию. Этот шаг не только уменьшает ввод, но и упрощает обслуживание кода. Когда это делается, это часто приносит чувство удовлетворения. Однако бывают случаи, когда этот процесс может быть компромиссом: извлеченный и обобщенный код мог бы работать медленнее, чем оригинал, оставляя меня желать оптимального баланса эффективности и производительности.
В отличие от Python, Java или C#, Rust позволяет создавать код с макросами во время компиляции. Что это значит? Это означает, что можно написать код, похожий на стандартный Rust, который затем будет расширен в другой код во время компиляции. Рассмотрим хорошо известный пример использования макроса println!
:
let name = "Adrian";
println!("Hello, {}", name);
Код предназначен для печати одной конкатенированной строки. В таких языках, как Java, среда выполнения будет анализировать первый аргумент, создавать местозаполнитель, а затем заменять его во время печати. Этот процесс требует дополнительных циклов ЦП и может привести к сбою. Напротив, Rust анализирует такое форматирование во время компиляции и создает следующий эквивалентный код:
let name = "Adrian";
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["Hello, ", "\n"],
&[::core::fmt::ArgumentV1::new_display(&name)],
),
);
};
В Rust такие макросы можно создать самостоятельно. Синтаксис трудно понять изначально, но крейт для вставки упрощает обучающую кривую. Рассмотрим пример, который включает значительный код шаблона:
struct ParserRevision {
revision_id: Option<String>,
revision_id_parent: Option<String>,
sha1: Option<String>,
timestamp: Option<String>,
contributor_id: Option<String>,
contributor_ip: Option<String>,
contributor_name: Option<String>,
}
impl ParserRevision {
fn with_revision_id(mut self, value: String) -> Self {
self.revision_id = Some(value);
self
}
fn with_revision_id_parent(mut self, value: String) -> Self {
self.revision_id_parent = Some(value);
self
}
fn with_sha1(mut self, value: String) -> Self {
self.sha1 = Some(value);
self
}
fn with_timestamp(mut self, value: String) -> Self {
self.timestamp = Some(value);
self
}
fn with_contributor_id(mut self, value: String) -> Self {
self.contributor_id = Some(value);
self
}
fn with_contributor_ip(mut self, value: String) -> Self {
self.contributor_ip = Some(value);
self
}
fn with_contributor_name(mut self, value: String) -> Self {
self.contributor_name = Some(value);
self
}
}
Код демонстрирует использование изменяемого объекта построителя, заполняющего его частные поля. Для каждого поля требуется новый метод, который выглядит идентично другим. Это можно улучшить, внедрив следующий метод для выполнения:
struct ParserRevision {
revision_id: Option<String>,
revision_id_parent: Option<String>,
sha1: Option<String>,
timestamp: Option<String>,
contributor_id: Option<String>,
contributor_ip: Option<String>,
contributor_name: Option<String>,
}
impl ParserRevision {
fn with(mut self, key: &str, value: String) -> Self {
match key {
"revision_id" => self.revision_id = Some(value),
"revision_id_parent" => self.revision_id_parent = Some(value),
"sha1" => self.sha1 = Some(value),
"timestamp" => self.timestamp = Some(value),
"contributor_id" => self.contributor_id = Some(value),
"contributor_ip" => self.contributor_ip = Some(value),
"contributor_name" => self.contributor_name = Some(value),
key => panic!("Key not found: {}", key),
}
self
}
}
Эта реализация имеет несколько недостатков, таких как использование panic!
и отсутствие надежных проверок действительных идентификаторов. Однако с помощью макросов Rust можно обойти эти проблемы. Мы можем создать предыдущий подробный код, который проверяет идентификаторы и не прибегает к панике:
macro_rules! with_functions {
($($name:ident,)*) => {
paste::paste! {
$(
fn [<with_ $name>](mut self, value: String) -> Self {
self.$name = Some(value);
self
}
)*
}
};
}
impl ParserRevision {
with_functions! {
revision_id,
revision_id_parent,
sha1,
timestamp,
contributor_id,
contributor_ip,
contributor_name,
}
}
Этот макрос берет список идентификаторов, разделенных запятыми, и генерирует соответствующий метод для каждого поля, ответственного за установку этого поля. Разве это не красивый подход?
Написание кода - это удовольствие, и генерация кода выводит это удовольствие на совершенно новый уровень. Можете ли вы видеть, что вы можете автоматизировать для дополнительной дозы радости в следующий раз?