Я только начинаю работать с Rust, и мне нужна помощь в понимании проверки типов в языке. Я пытаюсь внедрить регистратор активности для регистрации и регистрации продолжительности выполнения и результата функции. Вот упрощенная реализация:
struct Activity {
name: String,
start_time: std::time::Instant,
}
impl Activity {
fn begin(name: &str) -> Self {
println!("Activity started: {}", name);
Activity {
name: name.to_string(),
start_time: std::time::Instant::now(),
}
}
fn end_ok(&self) {
let duration = self.start_time.elapsed().as_millis();
println!("Activity ended: {}, Status: Success, Duration: {}", self.name, duration);
}
fn end_error<T: ToString>(&self, error: &T) {
let duration = self.start_time.elapsed().as_millis();
println!(
"Activity ended: {}, Status: Fail, Duration: {}, Message: {}",
self.name,
duration,
error.to_string()
);
}
}
type MyResult = Result<(), String>;
fn test1() -> Result<(), Box<dyn std::error::Error>> {
test2()?;
Ok(())
}
fn test2() -> MyResult {
Err("oops".to_string())
}
fn main() {
let activity = Activity::begin("TestActivity");
match test1() {
Ok(_) => activity.end_ok(),
Err(error) => activity.end_error(&error),
}
}
Хотя это работает, это кажется немного запутанным, поскольку мне приходится вручную анализировать каждый результат и строить цепочку результатов. Я бы предпочел использовать оператор ?
, но для этого требуется обернуть обработку результата. Я попытался использовать proc_macro_attribute
для обработки этой упаковки, но столкнулся с проблемой определения типа возвращаемого значения функции.
Я хочу узнать, что возвращает функциональный блок. Если это Result<_, _>
и Err
, я хочу вызвать end_error(error)
; иначе я хочу вызвать end_ok()
. Вот пример моей попытки макроса:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn, LitStr, ReturnType};
#[proc_macro_attribute]
pub fn activity_logger(attr: TokenStream, item: TokenStream) -> TokenStream {
let logger_name = parse_macro_input!(attr as LitStr).value();
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let fn_return_type = &input_fn.sig.output;
let output = match fn_return_type {
ReturnType::Type(_, _) => quote! {
fn #fn_name() #fn_return_type {
let activity = Activity::begin(#logger_name);
let result = (|| #fn_block)();
// C# like pseudo code start
if result is Result<_, _> {
match result {
Ok(_) => activity.end_ok(),
Err(error) => activity.end_error(error),
}
} else {
activity.end_ok();
}
// C# like pseudo code end
result
}
},
ReturnType::Default => quote! {
fn #fn_name() {
let activity = Activity::begin(#logger_name);
#fn_block
activity.end_ok();
}
},
};
output.into()
}
Есть ли способ определить во время выполнения, реализует ли объект черту Try
или является Result<_, _>
? Или макрос может в этом помочь? Я ценю любой совет!
Единственный найденный мною способ определить тип возвращаемого значения — это извлечь имя типа с помощью ReturnType::Type(_, ty)
или std::any::type_name
, но это не идеальный вариант, поскольку тип результата можно переименовать.
Я пытаюсь решить это с помощью черт
trait IsResult {
const IS_RESULT: bool;
}
impl<T, E> IsResult for Result<T, E> {
const IS_RESULT: bool = true;
}
impl<T> IsResult for T {
const IS_RESULT: bool = false;
}
trait Loggable {
fn can_log(&self) -> bool;
}
trait LoggableResult: Loggable {
fn can_log(&self) -> bool;
}
impl<T> Loggable for T {
fn can_log(&self) -> bool {
false
}
}
impl<T, E> LoggableResult for Result<T, E> {
fn can_log(&self) -> bool {
true
}
}
fn can_log(value: &dyn Loggable) -> bool {
value.can_log()
}
fn test() {
let test1_result = 123;
let test1 = can_log(&test1_result); // false
let test2_result: Result<(), String> = Result::Err("oops".to_string());
let test2 = can_log(&test2_result); // false
}
Макросы не имеют доступа к информации о типе, поскольку они раскрываются перед проверкой типа.
Это действительно возможно! (см. https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) Я создам декларативный макрос, чтобы продемонстрировать:
trait TypeDetector {
fn type_detect(&self, activity: &Activity);
}
impl<A, B: ToString> TypeDetector for Result<A, B> {
fn type_detect(&self, activity: &Activity) {
match self {
Ok(_) => {
activity.end_ok();
}
Err(error) => {
activity.end_error(error);
}
}
}
}
impl<A> TypeDetector for &A {
fn type_detect(&self, activity: &Activity) {
activity.end_ok();
}
}
macro_rules! activity_logger {
($log_name:expr, $vis:vis fn $name:ident() -> $out:ty {$($body:tt)*}) => {
$vis fn $name() -> $out {
let activity = Activity::begin($log_name);
let result = (||{$($body)*})();
(&result).type_detect(&activity);
result
}
}
}
Магия в этой строке
(&result).type_detect(&activity);
Если result
является результатом, это найдет реализацию TypeDetector
на Result
. В противном случае компилятор попытается вызвать type_detect
на &&result
, который будет использовать другую реализацию TypeDetector
.
Вот полный пример (игровая площадка):
struct Activity {
name: String,
start_time: std::time::Instant,
}
impl Activity {
fn begin(name: &str) -> Self {
println!("Activity started: {}", name);
Activity {
name: name.to_string(),
start_time: std::time::Instant::now(),
}
}
fn end_ok(&self) {
let duration = self.start_time.elapsed().as_millis();
println!("Activity ended: {}, Status: Success, Duration: {}", self.name, duration);
}
fn end_error<T: ToString>(&self, error: &T) {
let duration = self.start_time.elapsed().as_millis();
println!(
"Activity ended: {}, Status: Fail, Duration: {}, Message: {}",
self.name,
duration,
error.to_string()
);
}
}
type MyResult = Result<(), String>;
trait TypeDetector {
fn type_detect(&self, activity: &Activity);
}
impl<A, B: ToString> TypeDetector for Result<A, B> {
fn type_detect(&self, activity: &Activity) {
match self {
Ok(_) => {
activity.end_ok();
}
Err(error) => {
activity.end_error(error);
}
}
}
}
impl<A> TypeDetector for &A {
fn type_detect(&self, activity: &Activity) {
activity.end_ok();
}
}
macro_rules! activity_logger {
($log_name:expr, $vis:vis fn $name:ident() -> $out:ty {$($body:tt)*}) => {
$vis fn $name() -> $out {
let activity = Activity::begin($log_name);
let result = (||{$($body)*})();
(&result).type_detect(&activity);
result
}
}
}
activity_logger! { "TestActivity",
fn test1() -> Result<(), Box<dyn std::error::Error>> {
test2()?;
Ok(())
}
}
activity_logger! {"test2",
fn test2() -> MyResult {
Err("oops".to_string())
}
}
activity_logger! {"test3",
fn test3() -> u8 {
3
}
}
fn main() {
test1();
test3();
}
Хороший ход! Теперь все работает отлично. Большое спасибо! Я не знал, что для ссылочных типов можно реализовать черты, чтобы устранить неоднозначность.
Нельзя, ищите
specialization
, это пока не реализовано.