Rust, 为 traitA 的所有 struct 实现 traitB

3 minute read Published: 2022-05-27

有一个 traitA,希望所有实现该 traitstruct 都能被当成字符串用(即都实现 DisplayToString), traitA 本身已经有了一个将自己转换成字符串的公共逻辑,想实现 "impl Display for all traitA" 的效果

定义

traitA 如下

trait ResponseMessage: Sized {
    fn code(&self) -> u16;
    fn message(&self) -> &str;
}

它可以被这样转换为字符串,这个逻辑对所有实现 ResponseMessage 的结构相同

format!("{} {}\r\n", self.code(), self.message());

实现

第一个想法是:

impl<T: ResponseMessage> Display for T {
  //...
}

这样就能为所有 T 都实现同一个 Display

但编译不通过,理由是

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g., `MyStruct<T>`)
  --> src/response.rs:13:6
   |
13 | impl<T: ResponseMessage> ToString for T {
   |      ^ type parameter `T` must be used as the type parameter for some local type
   |
   = note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local
   = note: only traits defined in the current crate can be implemented for a type parameter

For more information about this error, try `rustc --explain E0210`.

主要问题在于,Display 是一个 foreign trait,如果我为所有 ResponseMessageimplDisplay,如果有一个 struct 先是 impl ResponseMessage,再 impl Display,则它就会有两个 Display 的实现,这便产生了冲突

在Discord上提问之后,得到的建议是

  1. traitA增加一个返回某 ADAPTER 类的默认方法,然后为这个 ADAPTER 类实现 traitB,例如

    pub trait ResponseMessage: Sized + Display {
        fn code(&self) -> u16;
        fn message(&self) -> &str;
        fn adapter(&self) -> DisplayAdapter {
            DisplayAdapter(&format!("{} {}\r\n", self.code(), self.message()))
        }
    }
    
    struct DisplayAdapter<'a>(&'a str);
    impl<'a> Display for DisplayAdapter<'a> {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "{}", self.0)
        }
    }
    

    之后只需要调用 ResponseMessage::adapter即可返回一个实现了Display的结构了

  2. 使用 macro 为每个结构生成 impl Display 的代码,如

    macro_rules! impl_display {
        ($structname: ty) => {
            impl Display for $structname {
                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                    write!(f, "{} {}\r\n", self.code(), self.message())
                }
            }        
        };
    }
    impl_display!(MyStruct) // for each Struct
    

最终采用了方案2.,因为在 MyStruct 的使用者看来,多一个 adapter 会增加理解的困难,且转接一个结构会有额外的性能开销