如何将结构体的生命周期限制为“父”结构体的生命周期?

7
我正在使用FFI编写一些Rust代码,针对具有所有权概念的C API(如果有关系,则为libnotmuch API)。
API的主要入口点是Database;我可以从Database创建Query对象。它为数据库和查询(以及许多其他对象)提供了析构函数。
但是,Query不能超出其创建的Database的范围。数据库析构函数将销毁任何未销毁的查询等,此后查询析构函数不起作用。
到目前为止,我已经将基本部件组合起来 - 我可以创建数据库和查询,并对它们执行操作。但是我在编码生命周期限制方面遇到了困难。
我正在尝试做这样的事情:
struct Db<'a>(...) // newtype wrapping an opaque DB pointer
struct Query<'a>(...) // newtype wrapping an opaque query pointer

我有实现Drop的方法,它们调用底层的C析构函数。
然后有一个创建查询的函数:
pub fun create_query<?>(db: &Db<?>, query_string: &str) -> Query<?>

我不知道在?的位置上放什么,以便查询返回值不会超出Db的生命周期。
我该如何为此API建模生命周期约束?
1个回答

7
当您想将输入参数的生命周期绑定到返回值的生命周期时,您需要在函数中定义一个生命周期参数,并在输入参数和返回值的类型中引用它。您可以为此生命周期参数指定任何名称;通常,当参数较少时,我们只是将它们命名为'a'b'c等。
您的Db类型需要一个生命周期参数,但实际上不需要:因为Db并不会引用已有对象,所以它没有生命周期的约束。
为了正确强制使DbQuery更长寿,我们必须在借用指针上写入'a,而不是刚刚删除的Db上的生命周期参数。
pub fn create_query<'a>(db: &'a Db, query_string: &str) -> Query<'a>

然而,这还不够。如果你的新类型完全没有引用它们的'a 参数,你会发现一个 Query 实际上可以比 Db 活得更久:

Editor's note: This code no longer compiles since Rust 1.0. You must use 'a in some way in the body of Query.

struct Db(*mut ());
struct Query<'a>(*mut ());  // '

fn create_query<'a>(db: &'a Db, query_string: &str) -> Query<'a> {  // '
    Query(0 as *mut ())
}

fn main() {
    let query;
    {
        let db = Db(0 as *mut ());
        let q = create_query(&db, "");
        query = q; // shouldn't compile!
    }
}

这是因为在Rust 1.0之前,生命周期参数是双变的,也就是编译器可以将参数替换为更长或更短的生命周期以满足调用者的要求。

当你在结构体中存储一个借用指针时,生命周期参数被视为协变:这意味着编译器可以将参数替换为更短的生命周期,但不能替换为更长的生命周期。

我们可以通过向结构体添加PhantomData标记来手动请求编译器将您的生命周期参数视为协变:

use std::marker::PhantomData;

struct Db(*mut ());
struct Query<'a>(*mut (), PhantomData<&'a ()>);

fn create_query<'a>(db: &'a Db, query_string: &str) -> Query<'a> {    // '
    Query(0 as *mut (), PhantomData)
}

fn main() {
    let query;
    {
        let db = Db(0 as *mut ());
        let q = create_query(&db, ""); // error: `db` does not live long enough
        query = q;
    }
}

现在,编译器可以正确地拒绝赋值给query,因为它的生命周期比db长。
额外收获: 如果我们将create_query更改为Db的方法而不是自由函数,我们可以利用编译器的生命周期推断规则,根本不需要在create_query上写'a
use std::marker::PhantomData;

struct Db(*mut ());
struct Query<'a>(*mut (), PhantomData<&'a ()>);

impl Db {
    //fn create_query<'a>(&'a self, query_string: &str) -> Query<'a>
    fn create_query(&self, query_string: &str) -> Query {
        Query(0 as *mut (), PhantomData)
    }
}

fn main() {
    let query;
    {
        let db = Db(0 as *mut ());
        let q = db.create_query(""); // error: `db` does not live long enough
        query = q;
    }
}

当一个方法有一个 `self` 参数时,编译器会更倾向于将该参数的生命周期与结果链接起来,即使还有其他具有生命周期的参数。然而,对于自由函数,只有一个参数具有生命周期时才可以推断。在这里,因为 `query_string` 参数是类型为 `&'a str` 的,有两个具有生命周期的参数,所以编译器无法推断我们想要将结果与哪个参数链接起来。

谢谢!有了这个设计,我用错误生命周期编写的测试函数现在被编译器正确拒绝。 - Michael Ekstrand
1
这已经过时了,但是一般的模式仍然适用。现在 &'a T&'a mut T'a 上始终是协变的,因此 std::marker::ContravariantLifetime 不再存在。但是您必须使用声明的任何类型参数,因此现在需要使用 std::marker::PhantomData - cbarrick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接