【问题标题】:Variable parameterised over a trait not a struct?通过特征而不是结构参数化的变量?
【发布时间】:2016-12-29 16:43:04
【问题描述】:

我正试图围绕 Rust 的泛型。我正在写一些东西来从不同的网站中提取 HTML。我想要的是这样的:

trait CanGetTitle {
    fn get_title(&self) -> String;
}

struct Spider<T: CanGetTitle> {
    pub parser: T
}

struct GoogleParser;
impl CanGetTitle for GoogleParser {
    fn get_title(&self) -> String {
        "title from H1".to_string().clone()
    }
}

struct YahooParser;
impl CanGetTitle for YahooParser {
    fn get_title(&self) -> String {
        "title from H2".to_string().clone()
    }
}

enum SiteName {
    Google,
    Yahoo,
}

impl SiteName {
    fn from_url(url: &str) -> SiteName {
        SiteName::Google
    }
}

fn main() {
    let url = "http://www.google.com";
    let site_name = SiteName::from_url(&url);
    let spider: Spider<_> = match site_name {
        Google => Spider { parser: GoogleParser },
        Yahoo => Spider { parser: YahooParser }
    };

    spider.parser.get_title();    // fails
}

我收到一个关于 match 返回 Spiders 参数化两种不同类型的错误。它期望它返回Spider&lt;GoogleParser&gt;,因为这是模式匹配的第一臂的返回类型。

我如何声明spider 应该是任何Spider&lt;T: CanGetTitle&gt;

【问题讨论】:

    标签: generics rust polymorphism trait-objects


    【解决方案1】:

    我如何声明spider 应该是任何Spider&lt;T: CanGetTitle&gt;

    只是补充一点@Shepmaster 已经说过的内容,spider 不能是任何 Spider&lt;T&gt;,因为它必须完全是一个 Spider&lt;T&gt;。 Rust 使用单态化(解释为 here)实现泛型,这意味着它为使用的每个具体类型编译一个单独的多态函数版本。如果编译器不能为特定的调用站点推断出唯一的T,那么这是一个编译错误。在您的情况下,编译器推断该类型必须为Spider&lt;Google&gt;,但下一行尝试将其视为Spider&lt;Yahoo&gt;

    使用 trait 对象可以让您将所有这些都推迟到运行时。通过将实际对象存储在堆上并使用Box,编译器知道需要分配多少堆栈空间(只是Box 的大小)。但这会带来性能成本:当需要访问数据时会有额外的指针间接寻址,更重要的是,优化编译器无法内联虚拟调用。

    通常可以重新调整事物,以便您可以使用单态类型。在您的情况下,一种方法是避免临时分配给多态变量,并仅在您知道其具体类型的地方使用该值:

    fn do_stuff<T: CanGetTitle>(spider: Spider<T>) {
        println!("{:?}", spider.parser.get_title());
    }
    
    fn main() {
        let url = "http://www.google.com";
        let site_name = SiteName::from_url(&url);
        match site_name {
            SiteName::Google => do_stuff(Spider { parser: GoogleParser }),
            SiteName::Yahoo => do_stuff(Spider { parser: YahooParser })
        };
    }
    

    请注意,每次调用 do_stuff 时,T 都会解析为不同的类型。您只编写了do_stuff 的一个实现,但编译器将其单态化了两次——您调用它时使用的每种类型一次。

    如果您使用Box,则必须在Boxvtable 中查找对parser.get_title() 的每次调用。但是这个版本通常会更快,因为它避免了查找的需要,并允许编译器在每种情况下内联parser.get_title() 的主体。

    【讨论】:

    • 嗯,很有趣。我认为在这种情况下,尽管我想要在站点之间执行的操作有很多共性,但唯一的区别是确切地使用哪些 HTML 选择器来提取我需要的数据,具体取决于站点等。跨度>
    • 在需要访问数据时以额外的指针间接为代价 => 实际上,这是您为此付出的最低成本。更大的成本是使用足够聪明的优化器来去虚拟化调用,这会抑制内联,而内联是优化的关键促成因素。因此,虽然额外的指针取消引用/虚拟调用的成本非常小,但内联和优化的损失(在紧密循环中)确实非常昂贵。
    • @MatthieuM。谢谢,做了一些调整以明确这一点。
    【解决方案2】:

    我如何声明spider 应该是任何Spider&lt;T: CanGetTitle&gt;

    你不能。简而言之,编译器不知道要分配多少空间来在堆栈上存储spider

    相反,您需要使用trait object:Box&lt;CanGetTitle&gt;:

    impl<T: ?Sized> CanGetTitle for Box<T>
    where
        T: CanGetTitle,
    {
        fn get_title(&self) -> String {
            (**self).get_title()
        }
    }
    
    fn main() {
        let innards: Box<CanGetTitle> = match SiteName::Google {
            SiteName::Google => Box::new(GoogleParser),
            SiteName::Yahoo => Box::new(YahooParser),
        };
        let spider = Spider { parser: innards };
    }
    

    【讨论】:

    • 我还在为此苦苦挣扎。它会与多个特征一起工作吗?我需要ParsePageGetQuery 等内容,并且需要可以扩展的内容以涵盖所有需要实现的特征。
    • @jbrown 为什么你认为它不适用于多个特征?
    • 出于某种原因,我还需要将?Sized 添加到Spider 中,就像在struct Spider&lt;T: ?Sized + CanGetTitle &gt; 中一样。很高兴知道这一点,非常感谢。
    • @jbrown:?Sized 对于具体的T 而言不是必需的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-24
    • 1970-01-01
    • 2011-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多