具有关联类型的工厂方法

Posted

技术标签:

【中文标题】具有关联类型的工厂方法【英文标题】:Factory method with associated type 【发布时间】:2020-05-12 17:11:26 【问题描述】:

我正在尝试实现一个工厂方法,该方法返回具有关联类型的Service。我让它在没有关联类型的情况下工作,但是一旦我添加它,无论我如何按摩它,我都无法让它编译..

这是Service

trait QType 

trait Service 
    type Query: QType;

    fn sanitize(&self, query: &str) -> Result<Self::Query, String>;

    fn run(&self, query: &Self::Query) -> Result<(), String>;

所以想法是sanitize 函数返回Query 的一个实例,然后可以将其传递给run 函数。

工厂看起来像这样(不编译):

fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>> 
    match name 
        "amazon" => Box::new(amzn::Amazon ),
        other => panic!("Invalid service ", other),
    

现在我这里只有一个服务,我可以在签名中指定类型参数——这将使它编译——但我​​想要一个通用工厂方法并添加更多服务。

这是Amazon服务的实现:

mod amzn 
    use super::*;

    pub struct Amazon 

    pub struct Product 
        name: String,
    

    impl QType for Product 

    impl Service for Amazon 
        type Query = Product;
        fn sanitize(&self, query: &str) -> Result<Product, String> 
        fn run(&self, query: &Product) -> Result<(), String> 
    

编译器说:

错误[E0271]:类型不匹配解析`::Query == Q` --> src/main.rs:9:21 | 9 | “亚马逊” => Box::new(amzn::Amazon ), | ^^^^^^^^^^^^^^^^^^^^^^^^^ 预期类型参数,找到 struct `amzn::Product` | =注意:预期类型`Q` 找到类型`amzn::Product` = help: 类型参数必须被约束以匹配其他类型 = 注意:有关更多信息,请访问 https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters = 注意:强制转换为对象类型 `dyn Service` 是必需的

基于此错误消息,我不确定如何指定类型参数。我已经尝试提取Amazon 的创建并为其提供显式类型参数,但这只会留下不同的错误。此外,在本书链接的第 10.02 章之后,并没有对关联类型的案例进行任何解释。最后,我也尝试了RFC-1598: Generic Associated Types的路由,但我无法编译,也不确定我是否真的需要它。

另外请注意,我添加了 Box 包装器和 QType 限制,基于其他关于类似问题的答案,但我可能完全走错了路..

非常感谢任何帮助。

【问题讨论】:

【参考方案1】:

此签名无法实现:

fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>>

关联类型始终由实现类型唯一确定。 IE。 Service 的每个实现只选择一个关联类型 Query

这与factory 不一致,后者让调用者决定关联的类型应该是什么。应该清楚地看到,如果您使用不是 ProductQ 调用 factory,那么 match 表达式中的代码将不再进行类型检查。

您可以通过修复Query 的选择来完成这项工作:

fn factory(name: &str) -> Box<dyn Service<Query = Product>> 
    match name 
        "amazon" => Box::new(amzn::Amazon ),
        other => panic!("Invalid service ", other),
    


如果您希望调用者选择类型,那么您需要找到一种方法,以便函数体适用于Q 的任何选择。例如,您可以将构造与 QType 特征关联:

trait QType 
    fn create_service(name: &str) -> Option<Box<dyn Service<Query = Self>>>;


fn factory<Q: QType>(name: &str) -> Box<dyn Service<Query = Q>> 
    Q::create_service(name).expect("Invalid service")

并为您的类型实现:

impl QType for Product 
    fn create_service(name: &str) -> Option<Box<dyn Service<Query = Self>>> 
        match name 
            "amazon" => Some(Box::new(amzn::Amazon )),
            other => None,
        
    

【讨论】:

谢谢,这真的帮助我理解了如何设置Q: QType 约束并没有真正的意义。 非常感谢!是否可以在不具体说明调用站点的类型参数的情况下使用它?我试过let service = factory("amazon"); service.run(&amp;service.sanitize("usb charger").unwrap()).unwrap();,但编译器希望指定Q。如果我将它设置为Product,它就可以工作,但我更愿意保持通用【参考方案2】:

我认为工厂方法可以使用动态调度实现,但没有关联类型。

我在rust playground 中使用您的示例实现了该模式。

具有动态调度的 Service 的特征现在如下所示:

trait Service 
    fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String>;

    fn run(&self, query: &dyn QType) -> Result<(), String>;

注意方法参数使用 trait 对象而不是具体类型。

由于我们实际上需要恢复具体类型,因此我们将使用 Any trait 和 AsAny 策略,以便能够从 trait 对象向下转换为具体类型。

use std::any::Any;

trait QType: Any + AsAny 

trait AsAny 
    fn as_any(&self) -> &dyn Any;


impl<Q: QType> AsAny for Q 
    fn as_any(&self) -> &dyn Any  
        self
    


fn downcast_ref_qtype<Q: QType> (qtype: &dyn QType) -> Result<&Q, String> 
    qtype.as_any().downcast_ref::<Q>().ok_or_else(|| "QType not supported".to_owned())

在具体实现中,我们可以使用这个 downcast_ref_qtype 方法来取回我们的具体类型:

    impl Service for Amazon 
        fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String> 
            // sanitize implementation
            // ...
        
            let product = Product 
                name: query.to_owned()
            ;
        
            Ok(Box::new(product))
        
        fn run(&self, query: &dyn QType) -> Result<(), String> 
            let product: &Product = downcast_ref_qtype(query)?;
            
            // run implementation
            // ...
            Ok(())
        
    

由于 Service trait 不再有关联类型,现在我们可以实现我们的工厂方法:

fn factory(name: &str) -> Box<dyn Service> 
    match name 
        "amazon" => Box::new(amzn::Amazon ),
        other => panic!("Invalid service ", other),
    


fn main() 
    let service = factory("amazon");
    
    service.run(service.sanitize("usb charger").unwrap().as_ref()).unwrap();

总而言之,我们将关联类型隐藏在 run 方法的具体实现中,并在调用 downcast 函数时定义它。

编辑:如果你仍然想要你的关联类型,我找到了一种添加另一个层的方法,这反过来又可以让你在需要时仍然使用你的 trait 和静态调度。

我们添加另一个具有关联类型的特征。

trait ServiceImpl 
    type Q: QType;

    fn sanitize(&self, query: &str) -> Result<Self::Q, String>;

    fn run(&self, query: &Self::Q) -> Result<(), String>;

我们添加了一个全面的实现,它为任何 ServiceImpl 实现者实现 Service trait 并负责向下转换。

impl <SI: ServiceImpl> Service for SI 
    fn sanitize(&self, query: &str) -> Result<Box<dyn QType>, String> 
        Ok(Box::new(ServiceImpl::sanitize(self, query)?))
    

    fn run(&self, query: &dyn QType) -> Result<(), String> 
        let query = downcast_ref_qtype::<SI::Q>(query)?;

        ServiceImpl::run(self, query)
    

现在我们只需要实现 ServiceImpl 并且结构体既可以用作 trait 对象也可以用于静态调度

impl ServiceImpl for Amazon 
        type Q = Product;

        fn sanitize(&self, query: &str) -> Result<Product, String> 
            // sanitize implementation
            // ...
        
            let product = Product 
                name: query.to_owned()
            ;
        
            Ok(product)
        
        fn run(&self, query: &Product) -> Result<(), String> 
            let product: &Product = downcast_ref_qtype(query)?;
            
            // run implementation
            // ...
            Ok(())
        
    

您可以在此处查看完整示例:rust playground

如果有帮助请告诉我:)

最好的问候, CHBS

【讨论】:

以上是关于具有关联类型的工厂方法的主要内容,如果未能解决你的问题,请参考以下文章

具有通用方法和继承的打字稿工厂,错误不可分配给类型

工厂模式

设计模式 抽象工厂模式

工厂类模式

抽象工厂模式的优缺点和适用场景

2-工厂模式